dry-struct How to conditionally validate one attribute? - ruby

I'm using dry-types and dry-struct and I would like to have a conditional validation.
for the class:
class Tax < Dry::Struct
attribute :tax_type, Types::String.constrained(min_size: 2, max_size: 3, included_in: %w[IVA IS NS])
attribute :tax_country_region, Types::String.constrained(max_size: 5)
attribute :tax_code, Types::String.constrained(max_size: 10)
attribute :description, Types::String.constrained(max_size: 255)
attribute :tax_percentage, Types::Integer
attribute :tax_ammount, Types::Integer.optional
end
I want to validate tax_ammount as an Integer and mandatory if `tax_type == 'IS'.

dry-struct is really for basic type assertion and coercion.
If you want more complex validation then you probably want to implement dry-validation as well (as recommended by dry-rb)
See Validating data with dry-struct which states
Please don’t. Structs are meant to work with valid input, it cannot generate error messages good enough for displaying them for a user etc. Use dry-validation for validating incoming data and then pass its output to structs.
The conditional validation using dry-validation would be something like
TaxValidation = Dry::Validation.Schema do
# Could be:
# required(:tax_type).filled(:str?,
# size?: 2..3,
# included_in?: %w(IVA IS NS))
# but since we are validating against a list of Strings I figured the rest was implied
required(:tax_type).filled(included_in?: %w(IVA IS NS))
optional(:tax_amount).maybe(:int?)
# rule name is of your choosing and will be used
# as the errors key (i just chose `tax_amount` for consistency)
rule(tax_amount:[:tax_type, :tax_amount]) do |tax_type, tax_amount|
tax_type.eql?('IS').then(tax_amount.filled?)
end
end
This requires tax_type to be in the %w(IVA IS NS) list;
Allows tax_amount to be optional but if it is filled in it must be an Integer (int?) and;
If tax_type == 'IS' (eql?('IS')) then tax_amount must be filled in (which means it must be an Integer based on the rule above).
Obviously you can validate your other inputs as well but I left these out for the sake of brevity.
Examples:
TaxValidation.({}).success?
#=> false
TaxValidation.({}).errors
# => {:tax_type=>["is missing"]}
TaxValidation.({tax_type: 'NO'}).errors
#=> {:tax_type=>["must be one of: IVA, IS, NS"]}
TaxValidation.({tax_type: 'NS'}).errors
#=> {}
TaxValidation.({tax_type: 'IS'}).errors
#=> {:tax_amount=>["must be filled"]}
TaxValidation.({tax_type: 'IS',tax_amount:'NO'}).errors
#=> {:tax_amount=>["must be an integer"]}
TaxValidation.({tax_type: 'NS',tax_amount:12}).errors
#=> {}
TaxValidation.({tax_type: 'NS',tax_amount:12}).success?
#=> true

There is an alternative solution - without validation rules that essentially duplicate the struct attributes.
module TaxChore
class BaseTax < Dry::Struct
attribute :type, Types::String.enum('IVA','NS')
# ...
attribute? :amount, Types::Integer.default(0)
end
class MandatoryTax < BaseTax
attribute :type, Types::String.enum('IS')
attribute :amount, Types::Integer
end
Tax = BaseTax | MandatoryTax
def self.run
tax = Tax.(type: 'IVA')
p tax
tax = Tax.(type: 'IVA', amount: 21)
p tax
tax = Tax.(type: 'IS', amount: 42)
p tax
begin
tax = Tax.(type: 'IS')
p tax
rescue Dry::Struct::Error => e
puts e
end
end
run
end
Outputs:
#<TaxChore::BaseTax type="IVA" amount=0>
#<TaxChore::BaseTax type="NS" amount=21>
#<TaxChore::MandatoryTax type="IS" amount=42>
[TaxChore::MandatoryTax.new] :amount is missing in Hash input
(N.B: I've removed the redundant tax_ prefix, since tax.type is just as clear but shorter.)

Related

ruby initialize method arguments mismatch

I'm writing an app in Sinatra, using activerecord, so I guess my question is the same as in Rails.
class Entry < ActiveRecord::Base
require 'date'
belongs_to :bankaccount
end
class Recurrent < Entry
attr_accessor :date_1, :date_2, :monthly_entry
def initialize (date_1, date_2)
#date_1 = date_1 # format DateTime.new(2020,12,5)
#date_2 = date_2
end
# other methods
When I run this code, I get :
>> date_1 = DateTime.new(2020,12,5)
>> date_2 = DateTime.new(2021,11,5)
>> recurrent = Recurrent.new(date_1, date_2)
**ArgumentError (wrong number of arguments (given 2, expected 0..1))**
and when I remove the arguments, I get this error message:
>> recurrent = Recurrent.new
**ArgumentError (wrong number of arguments (given 1, expected 2))**
When I do this in plain ruby and run it in irb, so without activerecord, it works fine.
According to the documentation:
Creation
Active Records accept constructor parameters either in a hash or as a block. The hash method is especially useful when you're receiving the data from somewhere else, like an HTTP request. It works like this:
user = User.new(name: "David", occupation: "Code Artist")
user.name # => "David"
You can also use block initialization:
user = User.new do |u|
u.name = "David"
u.occupation = "Code Artist"
end
And of course you can just create a bare object and specify the attributes after the fact:
user = User.new
user.name = "David"
user.occupation = "Code Artist"
So, ActiveRecord objects allow three different kinds of creating them:
No argument, set attributes later.
Block argument.
One Hash argument.
They don't allow two arguments.

How do I validate float values so that nothing but a float can be saved before submitting a form in ruby on rails 4?

I need a way to only allow values like:
Ok: 23.55, 232.43, 300.34 2.34
Not ok: 23.4, 43.344, 343.454, 230, 34
I have a regex in my model but it seems to allow me to save values like 200, 344, 23. I need to restrict things so that I'm only allowed to submit form when values are entered in the format of my Ok list.
Here is my model:
class Garment
include ActiveAttr::Model
#include ActiveModel::Validations
extend CarrierWave::Mount
attribute :price
mount_uploader :image, ImageUploader
price_regex = /\A(?:[1-9]+[0-9]*|0)(?:\.[0-9]{2})?\z/
validates :price, :presence => true,
:numericality => { :less_than => 301.00, :greater_than => 0.00 },
:format => {
:with => price_regex,
:message => "Price must be entered in the correct format e.g. 23.45, 203.43 not 43.3 or 234.5"
}
This is how I save the price entered into the form field:
def create
#garment = Garment.new(params[:garment])
if #garment.valid?
garment = Parse::Object.new("Garments")
garment["price"] = params[:garment][:price].to_f
garment.save
flash[:success] = "Garment successfully added to store!"
redirect_to '/adminpanel/show'
else
render "new"
end
end
I thought my regex was fine but I think I may need to tweak it more. I was wondering maybe I could some how check the value for a decimal and if it hasn't got one add one with 2 zeros after it before it is saved.
However I think the easiest most sensible way would be to do something before the actual form is submitted.
Would appreciate some help
Thanks for your help
Try a custom validator rather than a regex?
validate :valid_price_format
def valid_price_format
unless price.split('.')[1].try(:length) == 2
self.errors.add(:price, I18n.t('.invalid_format') )
end
end
Edited based on comments.
You can scope your translation if it's looking in the wrong place:
I18n.t('en.my.translation.location.invalid_format')
or
I18n.t('invalid_format', scope: 'en.my.translation.location')
Floats have quirks See Here.
Also 23.4 is a valid float. floats do not hold onto trailing zeros past the tenths place so 23.40 comes out as 23.4. You are better off storing prices as integer in cents e.g.
def price=(money)
#to_d returns a BigDecimal you could use to_f if you prefer
self.price = money.to_d * 100 if money
end
def price
#to_d returns a BigDecimal you could use to_f if you prefer
#bu BigDecimal is more accurate in comparisons
price.to_d / 100 if price
end
def display_price
#this will retun the price as in $XXXX.XX format as a String
sprintf("$%0.2f",price.to_f)
end
This way when you set it it will automatically convert it to an Integer and when using the getter method it will return a BigDecimal. Optionally you could leave off display_price method and use the helper method number_to_currency(price) in your views which will also add in commas and other configurable items. See number_to_currency
Also if you decide to forgo the above you can store them as decimals with the appropriate format using a migration like
add_column :table_name,:price, :decimal, precision: 8, scale: 2
Which will store them with 2 decimal places.
Update as a note
garment["price"] = params[:garment][:price].to_f
means that even if the user enters 12.00 it will be passed to save as 12.0 because of what is stated above. So you are assuming all prices do not end with a 0. Otherwise the next line
garment.save
Will fail silently and it will not be saved in the manor that you expect.
Also you are on rails 4 but not using strong_parameters?
#garment = Garment.new(params[:garment])
shouldn't this be
#garment = Garment.new(garment_params)
where garment_params utilizes a require and permit statement.
/^[1-9]*[0-9]+\.[0-9][0-9]$/ matches the "OK" values and rejects the "Not OK" values.
Share and enjoy.

Object's associated collection always sums to 0 during validation (ActiveRecord)

I am recording financial Interactions, modelled as an even number of LedgerEntries and each overall Interaction must sum out to zero.
class Interaction < ActiveRecord::Base
has_many :entries, class_name: 'LedgerEntry'
validate :entries_must_sum_to_zero
def balance
credit = self.entries.sum(:credit)
debit = self.entries.sum(:debit)
return credit - debit
end
protected
def entries_must_sum_to_zero
if self.entries.count.odd?
errors.add(:entries, "There must be an even number of entries.")
end
if self.balance != 0
errors.add(:entries, "Entries must sum to zero.")
end
end
end
and
class LedgerEntry < ActiveRecord::Base
validates_numericality_of :credit, greater_than_or_equal_to: 0
validates_numericality_of :debit, greater_than_or_equal_to: 0
belongs_to :interaction
validate :one_of_credit_or_debit
protected
def one_of_credit_or_debit
if self.credit != 0 && self.debit != 0
errors.add(:credit, "You can't assign both a credit and debit to the one entry.")
end
end
end
The problem I have is that this test never fails.
it "should complain if an Interaction's balance is non-zero" do
d = LedgerEntry.create!(credit: 50.0)
expect {Interaction.create!(entries: [d])}.to raise_error ActiveRecord::RecordInvalid
end
during the execution of entries_must_sum_to_zero self.entries.count is always 0 and self.balance always returns 0.
How do I force the entries to be taken up before the validation method runs?
In your validation, you're using database operations to do the validation (i.e. count and sum), but the entries haven't been stored to the database yet and won't be until after the Interaction is saved and they can be stored with their foreign key.
However, the entries attribute can still be accessed and operated on due to the magic of ActiveRecord proxies. You just need to use operations that don't depend on going to the database, such as length instead of count and to_a.map(&:<attribute>).inject(&:+) instead of sum as in:
def balance
credit = self.entries.to_a.map(&:credit).inject(&:+)
debit = self.entries.to_a.map(&:debit).inject(&:+)
return credit - debit
end
Try testing it like this instead:
it "should complain if an Interaction's balance is non-zero" do
entry = LedgerEntry.new(:credit => 50.0)
interaction = Interaction.new(:entries => [entry])
expect {interaction.valid?}.to raise_error ActiveRecord::RecordInvalid
end

List dynamic attributes in a Mongoid Model

I have gone over the documentation, and I can't find a specific way to go about this. I have already added some dynamic attributes to a model, and I would like to be able to iterate over all of them.
So, for a concrete example:
class Order
include Mongoid::Document
field :status, type: String, default: "pending"
end
And then I do the following:
Order.new(status: "processed", internal_id: "1111")
And later I want to come back and be able to get a list/array of all the dynamic attributes (in this case, "internal_id" is it).
I'm still digging, but I'd love to hear if anyone else has solved this already.
Just include something like this in your model:
module DynamicAttributeSupport
def self.included(base)
base.send :include, InstanceMethods
end
module InstanceMethods
def dynamic_attributes
attributes.keys - _protected_attributes[:default].to_a - fields.keys
end
def static_attributes
fields.keys - dynamic_attributes
end
end
end
and here is a spec to go with it:
require 'spec_helper'
describe "dynamic attributes" do
class DynamicAttributeModel
include Mongoid::Document
include DynamicAttributeSupport
field :defined_field, type: String
end
it "provides dynamic_attribute helper" do
d = DynamicAttributeModel.new(age: 45, defined_field: 'George')
d.dynamic_attributes.should == ['age']
end
it "has static attributes" do
d = DynamicAttributeModel.new(foo: 'bar')
d.static_attributes.should include('defined_field')
d.static_attributes.should_not include('foo')
end
it "allows creation with dynamic attributes" do
d = DynamicAttributeModel.create(age: 99, blood_type: 'A')
d = DynamicAttributeModel.find(d.id)
d.age.should == 99
d.blood_type.should == 'A'
d.dynamic_attributes.should == ['age', 'blood_type']
end
end
this will give you only the dynamic field names for a given record x:
dynamic_attribute_names = x.attributes.keys - x.fields.keys
if you use additional Mongoid features, you need to subtract the fields associated with those features:
e.g. for Mongoid::Versioning :
dynamic_attribute_names = (x.attributes.keys - x.fields.keys) - ['versions']
To get the key/value pairs for only the dynamic attributes:
make sure to clone the result of attributes(), otherwise you modify x !!
attr_hash = x.attributes.clone #### make sure to clone this, otherwise you modify x !!
dyn_attr_hash = attr_hash.delete_if{|k,v| ! dynamic_attribute_names.include?(k)}
or in one line:
x.attributes.clone.delete_if{|k,v| ! dynamic_attribute_names.include?(k)}
So, what I ended up doing is this. I'm not sure if it's the best way to go about it, but it seems to give me the results I'm looking for.
class Order
def dynamic_attributes
self.attributes.delete_if { |attribute|
self.fields.keys.member? attribute
}
end
end
Attributes appears to be a list of the actual attributes on the object, while fields appears to be a hash of the fields that were predefined. Couldn't exactly find that in the documentation, but I'm going with it for now unless someone else knows of a better way!
try .methods or .instance_variables
Not sure if I liked the clone approach, so I wrote one too. From this you could easily build a hash of the content too. This merely outputs it all the dynamic fields (flat structure)
(d.attributes.keys - d.fields.keys).each {|a| puts "#{a} = #{d[a]}"};
I wasn't able to get any of the above solutions to work (as I didn't want to have to add slabs and slabs of code to each model, and, for some reason, the attributes method does not exist on a model instance, for me. :/), so I decided to write my own helper to do this for me. Please note that this method includes both dynamic and predefined fields.
helpers/mongoid_attribute_helper.rb:
module MongoidAttributeHelper
def self.included(base)
base.extend(AttributeMethods)
end
module AttributeMethods
def get_all_attributes
map = %Q{
function() {
for(var key in this)
{
emit(key, null);
}
}
}
reduce = %Q{
function(key, value) {
return null;
}
}
hashedResults = self.map_reduce(map, reduce).out(inline: true) # Returns an array of Hashes (i.e. {"_id"=>"EmailAddress", "value"=>nil} )
# Build an array of just the "_id"s.
results = Array.new
hashedResults.each do |value|
results << value["_id"]
end
return results
end
end
end
models/user.rb:
class User
include Mongoid::Document
include MongoidAttributeHelper
...
end
Once I've added the aforementioned include (include MongoidAttributeHelper) to each model which I would like to use this method with, I can get a list of all fields using User.get_all_attributes.
Granted, this may not be the most efficient or elegant of methods, but it definitely works. :)

Chaining datamapper relationships across different repositories

class A
include DataMapper::Resource
def self.default_repository_name
:alt_db
end
property :aid, Integer, :key => true
# other stuff
belongs_to :b, :model => 'B', :child_key => [ :bid ]
end
class B
include DataMapper::Resource
# this one is in the default repo
property :bid, Integer, :key => true
# other stuff
belongs_to :c, :model => 'C', :child_key => [ :cid ]
end
class C
include DataMapper::Resource
# this one is in the default repo
property :cid, Integer, :key => true
# other stuff
end
If I just have A and B, this works fine. If I add C, however, I get an error:
dm-core/model/property.rb:73:in `new': wrong number of arguments (4 for 3) (ArgumentError)
If I want to make a chain of relationships with DataMapper, so that I can give an ID in one place and get a piece of data that's, say, four tables away through a series of references to subsequent tables' primary key ID field, how can I do this?
EDIT: Digging into the DM source from the stack trace:
DataMapper.repository(other_repository_name) do
properties << klass.new(self, name, options, type)
end
That's where the error is raised. Indeed, in this case klass is a DataMapper Integer property, and it's initialize method only accepts three options (model, name, and an options hash).
This whole block is only executed because I'm using more than one repository, though B and C are in the same one so I don't know if that sheds any light on why it's erroring on the cid property.
EDIT2:
I have tried all permutations, and it appears that when you're chaining, once you cross a database-boundary, that must be the end of the chain. For example, since A is :alt_db and B is :default, B is as deep as I can go, regardless of whether C is :default, :alt_db, or a third option.
If instead both A and B were :default, or both were :alt_db, and then C were the opposite one, C would be as deep as I could go.
I don't understand this behavior really, though.
You found a bug actually. It's been fixed in master. You can try grabbing sources from git and see if it works.
Your code works fine for me.
irb(main):001:0> A.first.b.c
DEBUG - "(0.001168) SELECT "aid", "bid" FROM "as" ORDER BY "aid" LIMIT 1"
DEBUG - "(0.000337) SELECT "bid", "cid" FROM "bs" WHERE "bid" = 2 LIMIT 1"
DEBUG - "(0.000046) SELECT "cid" FROM "cs" WHERE "cid" = 3 LIMIT 1"
=> #<C #cid=3>
My gem is dm-core-1.1.0, you should check your version.
It turns out this was a small issue with DataMapper chaining across repositories. Submitted to them and it's allegedly been fixed already!
http://datamapper.lighthouseapp.com/projects/20609/tickets/1506-can-only-chain-up-to-first-time-changing-default-repository#ticket-1506-1

Resources