Farmbot-Web-App/app/lib/celery_script/fetch_celery.rb

129 lines
4.5 KiB
Ruby

require_relative "./cs_heap"
# Service object that:
# 1. Pulls out all PrimaryNodes and EdgeNodes for a sequence node (AST Flat IR form)
# 2. Stitches the nodes back together in their "canonical" (nested) AST
# representation
# THIS IS BASICALLY A SERIALIZER FOR COMPLEX DATA THAT RAILS CAN'T HANDLE BY
# DEFAULT.
module CeleryScript
class FetchCelery < Mutations::Command
private # = = = = = = =
# This class is too CPU intensive to make multiple SQL requests.
# To speed up querying, we create an in-memory index for frequently
# looked up attributes such as :id, :kind, :parent_id, :primary_node_id
def edge_nodes
@edge_nodes ||= Indexer.new(sequence.edge_nodes)
end
# See docs for #edge_nodes()
def primary_nodes
@primary_nodes ||= Indexer.new(sequence.primary_nodes)
end
# The topmost node is always `NOTHING`. The term "root node" refers to
# the node where (kind == "sequence") in a tree of nodes. We start recursion
# here and move down.
def entry_node
@entry_node ||= primary_nodes.by.kind["sequence"].try(:first)
end
# Create a hash and attach all the EdgeNodes to it. Creates a partial "args"
# property when converting from flat IR to canonical form.
# Does not attach primary (fully formed CeleryScript) nodes.
def attach_edges(node)
output = {}
(edge_nodes.by.primary_node_id[node.id] || [])
.map { |edge| output[edge.kind] = edge.value }
output
end
# Similar to attach_edges(node) but for fully formed CS nodes.
# Eg: Will attach a `coordinate` node to a `location` arg.
def attach_primary_nodes(node)
output = {}
(primary_nodes.by.parent_id[node.id] || []).select(&:parent_arg_name)
.map { |x| output[x.parent_arg_name] = recurse_into_node(x) }
output
end
def recurse_into_args(node)
{}.merge!(attach_edges(node)).merge!(attach_primary_nodes(node))
end
# If you don't do this in memory, you will get N+1s all over the place - RC
def find_by_id_in_memory(the_id)
primary_nodes.by.id[the_id].first
end
# Pass this method a PrimaryNode and it will return an array filled with
# that node's children (or an empty array, since body is always optional).
def get_body_elements(origin)
next_node = find_by_id_in_memory(origin.body_id)
results = []
until next_node.kind == "nothing"
results.push(next_node)
next_node = find_by_id_in_memory(next_node[:next_id])
end
results
end
# Top level function call for converting a single EdgeNode into a JSON
# document. Returns Ruby hash that conforms to CeleryScript semantics.
def recurse_into_node(node)
out = { kind: node.kind, args: recurse_into_args(node) }
body = get_body_elements(node)
if body.empty?
# Legacy sequences *must* have body on sequence. Others are fine.
out[:body] = [] if node.kind == "sequence"
else
out[:body] = body.map { |x| recurse_into_node(x) }
end
out[:comment] = node.comment if node.comment
return out
end
# Generates a hash that has all the other fields that API users expect,
# Eg: color, id, etc.
def misc_fields
return {
id: sequence.id,
created_at: sequence.created_at,
updated_at: sequence.updated_at,
args: Sequence::DEFAULT_ARGS,
color: sequence.color,
name: sequence.name,
}
end
public # = = = = = = =
NO_SEQUENCE = "You must have a root node `sequence` at a minimum."
required do
model :sequence, class: Sequence
end
def validate
# A sequence lacking a `sequence` node is a syntax error.
# This should never show up in the frontend, but *is* helpful for devs
# when debugging (and has caught quite a few bugs as well).
add_error :bad_sequence, :bad, NO_SEQUENCE unless entry_node
end
def execute
canonical_form = misc_fields.merge!(recurse_into_node(entry_node))
s = canonical_form.with_indifferent_access
# HISTORICAL NOTE:
# When I prototyped the variables declaration stuff, a few (failed)
# iterations snuck into the DB. Gradually migrating is easier than
# running a full blow table wide migration.
# - RC 3-April-18
has_scope = s.dig(:args, :locals, :kind) == "scope_declaration"
s[:args][:locals] = Sequence::SCOPE_DECLARATION unless has_scope
return s
end
end
end