I've written a method for an API call that updates a store menu. It takes JSON as input, in the following format:
{
"menu" : {
"item_groups" : [
{
"id" : 1,
"name" : "Drinks",
"items" : [
{
"id" : 1,
"name" : "Coffee",
"option_groups" : [
{
"id" : 1,
"name" : "sizes",
"options" : [
{
"id" : 1,
"name" : "small",
"price" : "$1.00"
},
{
"name" : "medium",
"price" : "$2.00"
},
]
}
]
}
]
}
]
}
}
So for that input JSON, assume that all objects exist in the database except for the medium $2.00
option, which is added since it has no associated id
.
To cut down on code, I'll simply describe the model:
- Each menu has many item groups
- Each item group has many items
- Each item has many option groups
- Each option group has many options
- Each option has a name and a price
There are also name, id, and priority attributes for each of these. The priority is simply an index of their order as they appear in the client view. Also each object has an appropriately named property representing the inverse relation to its parent (ex, menu.itemGroups[0].menu == menu
).
The code I wrote does the following things, in a nutshell:
- Updates the attributes of each object
- Infers object deletion by detecting objects that are not present in the JSON request, as compared to the database
- Infers object priority by the order in which the objects appear in the JSON request
- Creates new objects for which there is either no id in the JSON request, or if no object can be found with that id.
- Outputs the updated Menu object as JSON
def menu_params
params.require(:menu).permit(
item_groups: [
:id,
:name,
items: [
:id,
:name,
option_groups: [
:id,
:name,
options: [
:id,
:name,
:price
]
]
]
]
)
end
def update
deny_access and return unless user_can_edit_store_menu?(current_user, @store)
params_menu = menu_params
if !params_menu
render json: { message: "Nothing to update." }, status: :ok
return
end
# get a reference to this menu's item groups
db_itemGroups_array = @menu.itemGroups.to_ary
new_itemGroups_array = []
# loop through json item groups
itemGroupIndex = 0
params_menu[:item_groups].each { |params_itemGroup|
next if params_itemGroup[:items].empty?
# if id param doesn't exist
if (ig_id = params_itemGroup[:id]) != nil
# if an itemGroup with this id param can be found
db_itemGroup = db_itemGroups_array.find { |o| o.id == ig_id.to_i }
end
# create a new item group if there isn't one already
db_itemGroup ||= MenuItemGroup.new(menu: @menu)
db_itemGroup.name = params_itemGroup[:name]
new_itemGroups_array << db_itemGroup
# get the array of items
db_items_array = db_itemGroup.items.to_ary
new_items_array = []
# loop through json items
itemIndex = 0
params_itemGroup[:items].each { |params_item|
next if params_item[:option_groups].empty?
# check that param exists for item id
if (i_id = params_item[:id]) != nil
# search for a match in the database's cached array
db_item = db_items_array.find { |item| item.id == i_id.to_i }
end
# create a new item if there isn't one already
db_item ||= MenuItem.new(itemGroup: db_itemGroup)
db_item.name = params_item[:name]
new_items_array << db_item
# get the array of option groups
db_optionGroups_array = db_item.optionGroups.to_ary
new_optionGroups_array = []
# loop through json option groups
optionGroupIndex = 0
params_item[:option_groups].each { |params_optionGroup|
next if params_optionGroup[:options].empty?
# check that param exists for id
if (og_id = params_optionGroup[:id]) != nil
db_optionGroup = db_optionGroups_array.find { |optionGroup| optionGroup.id == og_id.to_i }
end
# create an optionGroup if there isn't one already
db_optionGroup ||= MenuItemOptionGroup.new(item: db_item)
db_optionGroup.name = params_optionGroup[:name]
new_optionGroups_array << db_optionGroup
# get array of options
db_options_array = db_optionGroup.options.to_ary
new_options_array = []
# loop through json options
optionIndex = 0
params_optionGroup[:options].each { |params_option|
# check that the param for id exists
if (o_id = params_option[:id]) != nil
db_option = db_options_array.find { |option| option.id == o_id.to_i }
end
# create the option and add it to the array if it isn't in there already
db_option ||= MenuItemOption.new(optionGroup: db_optionGroup)
new_options_array << db_option
# assign params data to the model
db_option.name = params_option[:name]
db_option.price = params_option[:price] || "$0.00"
db_option.priority = optionIndex+=1
db_option.save
}
# clean up options array
destroy_array = db_options_array - new_options_array
# if all options will be destroyed, just destroy the option group
if !destroy_array.empty? and new_options_array.empty? and db_options_array.count == destroy_array.count
db_optionGroup.destroy unless db_optionGroup.new_record?
else
destroy_array.each { |d| d.destroy }
db_optionGroup.priority = optionGroupIndex
db_optionGroup.save
end
optionGroupIndex+=1
}
# clean up option groups array
destroy_array = db_optionGroups_array - new_optionGroups_array
# save the item record or destroy it if there are no option groups
if !destroy_array.empty? and new_optionGroups_array.empty? and db_optionGroups_array.count == destroy_array.count
db_item.destroy unless db_item.new_record?
else
destroy_array.each { |d| d.destroy }
db_item.priority = itemIndex
db_item.save
end
itemIndex += 1
}
# clean up items array
destroy_array = db_items_array - new_items_array
# if the number of items in the db equals the number of items deleted, and there are no new items
if !destroy_array.empty? and new_items_array.empty? and db_items_array.count == destroy_array.count
db_itemGroup.destroy unless db_itemGroup.new_record?
else
destroy_array.each { |d| d.destroy }
db_itemGroup.priority = itemGroupIndex
db_itemGroup.save
end
itemGroupIndex += 1
}
# clean up item groups by figuring out what's left in the database and destroying it
destroy_array = db_itemGroups_array - new_itemGroups_array
destroy_array.each { |d| d.destroy }
# save the menu
@menu.save
render json: { menu: @menu.json }, status: :ok
end
The code works well, but I'm obsessed with making it better, easier to read, etc. I employed all my remedial knowledge of Rails to try to make this as efficient as possible, and I would love it if anyone pointed out the weak spots of this code.
I think I see potential for perhaps refactoring the repetitive nests into blocks or methods. I would love it if someone could point me in the right direction on the best way to go about this.