Rails strong parameters for infinitely nestable model - ruby-on-rails

I have a model, QueryElement, with a child class, QueryGroup.
QueryGroup can contain nested QueryElements.
Say QueryGroup has a propertyname, and QueryElement has a property filter (just for example)
So for strong parameters I have something like:
params.fetch(:query).permit(:name, :filter, :query_elements => [:name, :filter, :query_elements => [...]
And so on.
I could permit all (defeating the security of strong parameters, which I'd rather avoid), or manually step through the tree, which is much slower. That's my current approach.
Is there a better way?

Something like this:
REQUIRED = %i( name ).freeze
ALLOWED = (%i( filter query_elements ) + REQUIRED).freeze
MAX_DEPTH = 5
def ensure_params(hash, nest_level = 0) # ah I never come up with good names...
raise 'you went too deep man' if nest_level > MAX_DEPTH
hash.fetch_values(*REQUIRED)
hash[:query_elements] = ensure_params(hash[:query_elements], nest_level + 1) if hash[:query_elements]
hash.slice(*ALLOWED)
end
In IRB:
> ensure_params({ :filter => 2, :name => 'test', unpermitted_param: :something, :query_elements => { filter: 3, name: 'test', nested_unpermitted: 13 } })
# => {:filter=>2, :query_elements=>{:filter=>3, :name=>"test"}, :name=>"test"}
> ensure_params({ name: 1, query_elements: { notname: 1 } })
KeyError: key not found: :name
> MAX_DEPTH = 3
# => 3
> ensure_params({ name: 1, query_elements: { name: 1, query_elements: { name: 1, query_elements: { name: 1, query_elements: { name: 1, query_elements: { name: 1 } } } } }})
RuntimeError: you went too deep man
There is probably some improvements that could be done, like converting the keys to symbols, have a better error message to tell you at which nest level there is a missing key, etc.

Related

Ruby how to format nested hash

I have two queries I am running and iterating over both and my final hash is seen below. But, I want to have format on how the data is being stored in the hash that I'm creating or format it after I'm done creating it. But I am not sure how to achieve the desired format where the names fall under the same id as show below
desired format of example data:
[
{
id: 1,
accepted: false,
trans: 10234
names: [
{ name: "Joe", amount: "$1,698.00" },
{ name: "Smith", amount: "$674.24" },
]
},
{
id: 2,
accepted: true,
trans: 10234,
names: [
{ name: "Joe", amount: "$1,698.00" },
{ name: "Smith", amount: "$674.24" },
]
}
]
current format I have
[
{
:id => 1,
:accepted => false,
:trans => 8,
:name => "Smith",
:amount => 36.0
},
{
:id => 1,
:amount => false,
:trans => 8,
:name => "Joe",
:amount => 6.0
},
{
:id => 3,
:accepted => false,
:trans => 8,
:name => "Tom",
:amount => 34.0
},
{
:id => 3,
:accepted => false,
:trans=> 8,
:name => "Martha",
:amount => 4.0
}
],
[
{
:id => 2,
:accepted => true,
:trans => 7,
:name => "Bob",
:amount => 35.0
},
{
:id => 2,
:accepted => true,
:trans => 7,
:name => "John",
:amount => 5.0
}
]
logic for creating hash
imports = ListImports.limit(20).order(created_at: :DESC)
groups = imports.map{|import| ListImportGroup.where(list_import_id: import.id)}
pub_hash_true = []
pub_hash_false = []
hash = []
imports.map do |import|
hash << {
id: import.id,
trans: import.trans,
accepted: import.amount
}
end
hash.each do |import|
groups.flatten.each do |group|
accepted = import[:accepted]
num_transactions = import[:trans]
if accepted == false
pub_hash_false << {id: import[:id], accepted: accepted, trans: num_transactions, name: group.name, amount: group.amount}
else
pub_hash_true << {id: import[:id], accepted: accepted, trans: num_transactions, name: group.name, amount: group.amount}
end
end
end
# Note: You didn't specify what is the association between `ListImport` and `ListImportGroup`.
# However, I'm fairly sure you could be fetching this data via a JOIN query like below,
# rather than making up to 20 additional database calls to fetch the associated records.
imports = ListImports.limit(20).order(created_at: :DESC).includes(:list_import_group)
result = imports.map do |import|
{
id: import.id,
trans: import.trans,
accepted: import.amount,
names: import.list_import_groups.pluck(:name, :amount)
}
end
And if you do actually need to filter for imports where accepted is true or false, you could do something like this instead of building separate arrays manually:
accepted_imports = result.select { |import| import[:accepted] }
# and
rejected_imports = result.reject { |import| import[:accepted] }
# or even:
accepted_imports, rejected_imports = result.partition { |import| import[:accepted] }
You didn't specify the exact correspondence between the desired and current formats.
But I assume
For the entries with the same id, the values of accepted and trans are identical.
the desired amount for Joe in the current format is identical in the corresponding amount in the desired amount. (In your example, the former is 6.0 whereas the latter is "$1,698.00", which does not make sense.)
Then, the following would do the conversion. The array ahout is in the desired format.
# Let us assume "a1" is the original array in the "current format"
hout = {}
a1.flatten.map{|h|
h.slice(*(%i(id trans name amount accepted))).values
}.each{ |a|
hout[a[0]] = {id: a[0], accepted: a[4], trans: a[1], names: []} if !hout.key? a[0]
hout[a[0]][:names].push(
{name: a[2], amount: "$"+helper.number_with_precision(a[3], precision: 2, delimiter: ',')}
)
}
ahout = hout.values
You may want to sort ahout, if you like.
Note that I am assuming you are using Rails 5+. Otherwise, the method helper may not work. In that case, you can use sprintf or whatever formatting method.

Rails group and sum array of objects

A production has_many :production_lines,
production_line belongs_to :item,
item has_one :recipe,
recipe has_many :recipe_lines,
recipe_line belongs_to :item,
production_line and recipe line have attribute quantity. I need to group recipe_lines for a production by item, with quantity that equals to production_line.quantity * recipe_line.quantity
def item_quantities
array = production_lines.map do |p|
p.item.recipe.recipe_lines.map do |r|
{
item_id: r.item_id,
item_name: r.item.name,
quantity: r.quantity * p.quantity
}
end
end
array.flatten(1).group_by { |p| p[:item_id] }
.transform_values { |vals| vals.sum { |val| val[:quantity] } }
end
This returns:
item_quantities = {
1: 10,
2: 5
}
where key is item_id and value is quantity. Values are correct.
However I would like to return:
item_quantities = [
{
id: 1,
name: "Tomato",
quantity: 10,
},
{
id: 2,
name: "Carrot",
quantity: 5
}
]
How should I change my solution to achieve that?
First of all, your nested map followed by flatten(1) can be simplified by making the first map into flat_map. If you do this you could remove the flatten(1).
From this point your code is most of the way there, but you could make the following changes to get the desired output:
you can group by multiple attributes, name and id. In another language you might use a tuple for this. Ruby doesn't have tuples, so we can just use a len-2 array:
.group_by { |p| [p[:item_id], p[:item_name]] }
.transform_values { |vals| vals.sum { |val| val[:quantity] } }
At this point you have a hash mapping [id,name] tuple to quantity:
{ [1,"foo"] => 123, [2, "bar"] => 456 }
and you can coerce this to the desired data type using reduce (or each_with_object, if you prefer):
.reduce([]) do |memo, ((id, name), quantity)|
memo << {
id: id,
name: name,
quantity: quantity
}
end
The wierd looking ((id, name), quantity) is a kind of destructuring. See https://jsarbada.wordpress.com/2019/02/05/destructuring-with-ruby/ specifically the sections on "Destructuring Block Arguments" and "Destructuring Hashes".

ActiveModel::Errors when mass updating

On a single instance, validation would produce something like:
foo = Foo.new(price: -2)
foo.valid?
foo.errors
=> #<ActiveModel::Errors:0x00007fc66e670430
#base=#<Foo:0x00007fc6503f8658 id: nil, price: nil,
#details={:price=>[{:error=>:greater_than_or_equal_to, :value=>-0.2e1, :count=>0}]},
#messages={:price=>["must be greater than or equal to 0"]}>
Is there a rails way of obtaining the errors when using the update method?:
Foo.update([1, 2, 3], [{ price: 10 }, { price: -20 }, { price: 3 }])
Thank you!
Here is an example how to gather errors from the Model.update(...) method:
# first create a payload with ids and attributes
payload = { 1 => { price: 10 }, 2 => { price: -20 } }
# next update records
result = Foo.update(payload.keys, payload.values)
# the update method returns processed records
# in case of array it will return array of records
# iterate over all objects and find invalid
with_errors = result.map { |r| !r.errors.any? ? nil : r }.compact
# after compact the with_errors variable contains only invalid records.
What I did in the end:
class FooUpdateService
attr_reader :errors
def update(ids, values)
self.errors = ActiveModel::Errors.new(Foo.new)
Foo.update(ids, values).select(&:invalid?).each { |invalid_foo| self.errors.merge!(invalid_foo.errors) }
end
private
attr_writer :errors
end

Parameterized JSON key naming

I have the following JSON:
{
my_json: {
id: 1,
name: "John"
}
}
How can I customize key name via parameterized like:
def jsonize(custom_key="id")
{
my_json: {
"#{custom_key}": 1,
name: "John"
}
}
end
To be output with:
Scenario 1:
=> jsonize
OUTPUT:
{
my_json: {
id: 1,
name: "John"
}
}
Scenario 2:
=> jsonize("value")
OUTPUT:
{
my_json: {
value: 1,
name: "John"
}
}
You can use ":" to separate symbolic keys and values, use "=>" in your example:
def jsonize(custom_key="id")
{
my_json: {
"#{custom_key}" => 1,
name: "John"
}
}
end
The hash-rocket syntax has been in Ruby since ancient times:
{ :foo => 1, "bar" => 2 }
Ruby 1.9 (I think) introduced a new colon shortcut syntax just for symbols (while keeping the hash-rocket general for any key type):
{ foo: 1, "bar" => 2 }
Ruby 2.2 (I think) introduced the possibility of symbolizing a string in this syntax:
{ "foo": 1, "bar" => 2 }
All of these do the same thing. What you are doing is perfectly grammatical Ruby code -- in a sufficiently new Ruby. In older Rubies, you will need to use the old reliable hash-rocket syntax:
{ "foo".to_sym => 1, "bar" => 2 }
Now that you actually have a string, you can do normal interpolation:
{ "f#{'o' * 2}".to_sym => 1, "bar" => 2 }
In your case, you could write
{ "#{custom_key}".to_sym => 1 }
However, all of this is completely unnecessary, since you can just write simply this, in any Ruby:
{ custom_key.to_sym => 1 }
Even better, since you're just turning everything into JSON immediately after, you don't even need symbolised keys; so these two expressions will have identical results:
{ custom_key.to_sym => 1 }.to_json
{ custom_key => 1 }.to_json
(Also note that what you state as examples of JSON -- both input and output -- are, in fact, not JSON, nor would .to_json output such. In JSON, as opposed to plain JavaScript object literal, keys must be double-quoted, and that is how to_json would produce it. Your input is a Ruby valid Ruby hash, though.)
You can just convert it to symbol and use hash_rocket syntax you will get the expected result
def jsonize(custom_key = "id")
{
my_json: {
custom_key.to_sym => 1,
name: "John"
}
}
end
#=> jsonize('foo')
#=> {
#=> :my_json => {
#=> :foo => 1,
#=> :name => "John"
#=> }
#=> }

How would I save multiple records at once in Rails?

How would I save this array in one call with Rails?
tax_rates = [{
:income_from => 0
:income_to => 18200
:start => "01-07-2013"
:finish => "30-06-2014"
:rate => nil
:premium => nil
},{
:income_from => 18201
:income_to => 37000
:start => "01-07-2013"
:finish => "30-06-2014"
:rate => 0.19
:premium => nil
},{
:income_from => 18201
:income_to => 37000
:start => "01-07-2013"
:finish => "30-06-2014"
:rate => 0.19
:premium => nil
}]
Can I just call Rails.create(tax_rates)?
Also, is there a way to remove duplicate symbols so they look neater?
Your example is almost correct.
Use ActiveRecord::Persistence#create, which can accept an array of hashes as a parameter.
tax_rates = [
{
income_from: 0,
income_to: 18200,
start: "01-07-2013",
finish: "30-06-2014",
rate: nil,
premium: nil,
},
{
income_from: 18201,
income_to: 37000,
start: "01-07-2013",
finish: "30-06-2014",
rate: 0.19,
premium: nil,
},
# ...
]
TaxRate.create(tax_rates) # Or `create!` to raise if validations fail
A nice solution is to use the active record import gem. I recommend it over now built-in Rails bulk insert because it's more flexible in the options in case of constraint violation.
TaxRate.import(
[:income_from, :income_to, :start, :finish, :rate, :premium],
tax_rates
)
Its definitely better than my old answer which would trigger a db commit per entry in the array :)
Old answer:
tax_rates.map {|tax_rate| TaxRate.new(tax_rate).save }
This way you'll retrieve an Array with true or false to know which did succeed and which didn't.
If you want all of them to be saved .or, non of them to be saved even if one fails, you can use 'ActiveRecord::Base.transaction'
e.g.
ActiveRecord::Base.transaction do
tax_rate.each do |tax_rt|
TaxRate.new(tax_rt).save
end
end
I am not sure about rails < 4.2 but I have tried it in rails 4.2 you can simply do this
TaxRate.create(tax_rt)
Here is an example like yours:
a = []
a << B.new(:name => "c")
a << B.new(:name => "s")
a << B.new(:name => "e")
a << B.new(:name => "t")
The array is saved all at once with:
a.each(&:save)
This will call B#save on each item in the array.
use a gem 'fast_inserter': https://github.com/joinhandshake/fast_inserter
it generates a single sql query of thousand records.
movie_data = [1, 'Climates (Iklimler)', 'Clay Pauwel', 'Drama'],
[2, 'Tinpis Run', 'Andros Glazer', 'Comedy'],
[3, 'Naked City, The', 'Bethena Chatband', 'Mystery'],
[4, 'Small Time Crooks', 'Naomi Plom', 'Crime'],
[5, 'Shadowboxer', 'Georgeanne Widdicombe', 'Thriller']
params = {
table: 'movies',
static_columns: {
created_at: '0000-00-00 00:00:00',
updated_at: '0000-00-00 00:00:00',
},
options: {
timestamps: false,
unique: true,
check_for_existing: true
},
group_size: 100,
variable_columns: %w(id title director description),
values: movie_data
}
inserter = FastInserter::Base.new(params)
inserter.fast_insert

Resources