diff --git a/app/models/celery_script_settings_bag.rb b/app/models/celery_script_settings_bag.rb index cfd9b7cfd..c1a2bd71b 100644 --- a/app/models/celery_script_settings_bag.rb +++ b/app/models/celery_script_settings_bag.rb @@ -86,21 +86,21 @@ module CeleryScriptSettingsBag }.map { |(name, list)| Corpus.value(name, list) } CORPUS_ENUM = { - ALLOWED_AXIS: [ALLOWED_AXIS, BAD_AXIS], - ALLOWED_CHANNEL_NAMES: [ALLOWED_CHANNEL_NAMES, BAD_CHANNEL_NAME], - ALLOWED_MESSAGE_TYPES: [ALLOWED_MESSAGE_TYPES, BAD_MESSAGE_TYPE], - ALLOWED_OPS: [ALLOWED_OPS, BAD_OP], - ALLOWED_PACKAGES: [ALLOWED_PACKAGES, BAD_PACKAGE], - ALLOWED_PIN_MODES: [ALLOWED_PIN_MODES, BAD_ALLOWED_PIN_MODES], - AllowedGroupTypes: [ALLOWED_EVERY_POINT_TYPE, BAD_EVERY_POINT_TYPE], - AllowedPinTypes: [ALLOWED_PIN_TYPES, BAD_PIN_TYPE], - Color: [Sequence::COLORS, MISC_ENUM_ERR], - DataChangeType: [ALLOWED_CHAGES, MISC_ENUM_ERR], - LegalSequenceKind: [ALLOWED_RPC_NODES.sort, MISC_ENUM_ERR], - lhs: [ALLOWED_LHS_STRINGS, BAD_LHS], - PlantStage: [PLANT_STAGES, MISC_ENUM_ERR], - PointType: [ALLOWED_POINTER_TYPE, BAD_POINTER_TYPE], - resource_type: [ALLOWED_RESOURCE_TYPE, BAD_RESOURCE_TYPE], + ALLOWED_AXIS: [ALLOWED_AXIS, BAD_AXIS], + ALLOWED_CHANNEL_NAMES: [ALLOWED_CHANNEL_NAMES, BAD_CHANNEL_NAME], + ALLOWED_MESSAGE_TYPES: [ALLOWED_MESSAGE_TYPES, BAD_MESSAGE_TYPE], + ALLOWED_OPS: [ALLOWED_OPS, BAD_OP], + ALLOWED_PACKAGES: [ALLOWED_PACKAGES, BAD_PACKAGE], + ALLOWED_PIN_MODES: [ALLOWED_PIN_MODES, BAD_ALLOWED_PIN_MODES], + AllowedGroupTypes: [ALLOWED_EVERY_POINT_TYPE, BAD_EVERY_POINT_TYPE], + AllowedPinTypes: [ALLOWED_PIN_TYPES, BAD_PIN_TYPE], + Color: [Sequence::COLORS, MISC_ENUM_ERR], + DataChangeType: [ALLOWED_CHAGES, MISC_ENUM_ERR], + LegalSequenceKind: [ALLOWED_RPC_NODES.sort, MISC_ENUM_ERR], + lhs: [ALLOWED_LHS_STRINGS, BAD_LHS], + PlantStage: [PLANT_STAGES, MISC_ENUM_ERR], + PointType: [ALLOWED_POINTER_TYPE, BAD_POINTER_TYPE], + resource_type: [ALLOWED_RESOURCE_TYPE, BAD_RESOURCE_TYPE], }.each { |(name, list)| Corpus.enum(name, *list) } def self.e(symbol) @@ -232,7 +232,12 @@ module CeleryScriptSettingsBag end, }, package: { - defn: [e(:ALLOWED_PACKAGES)], + defn: [v(:string)], + # `package` has an ambiguous intent depending on who is using the arg + # (FBOS vs. API). Corpus-native enums cannot be used for validation + # outside of the API. If `package` _was_ declared as a native enum (rather + # than a string), it would cause false type errors in FE/FBJS. + blk: -> (node) { manual_enum(ALLOWED_PACKAGES, node, BAD_PACKAGE) }, }, axis: { defn: [e(:ALLOWED_AXIS)], @@ -281,12 +286,13 @@ module CeleryScriptSettingsBag change_ownership: { body: [:pair], tags: [:function, :network_user, :disk_user, :cuts_power, :api_writer], - docs: "Not a commonly used node. May be removed without notice." + blk: -> (node) { raise "Never." }, + docs: "Not a commonly used node. May be removed without notice.", }, channel: { args: [:channel_name], tags: [:data], - docs: "Specifies a communication path for log messages." + docs: "Specifies a communication path for log messages.", }, check_updates: { args: [:package], @@ -294,11 +300,11 @@ module CeleryScriptSettingsBag }, coordinate: { args: [:x, :y, :z], - tags: [:data, :location_like] + tags: [:data, :location_like], }, dump_info: { tags: [:function, :network_user, :disk_user, :api_writer], - docs: "Sends an info dump to server administrators for troubleshooting." + docs: "Sends an info dump to server administrators for troubleshooting.", }, emergency_lock: { tags: [:function, :firmware_user, :control_flow], @@ -309,7 +315,7 @@ module CeleryScriptSettingsBag every_point: { args: [:every_point_type], tags: [:data, :list_like, :control_flow], - docs: "Experimental node used for iteration." + docs: "Experimental node used for iteration.", }, execute_script: { args: [:label], @@ -503,11 +509,11 @@ module CeleryScriptSettingsBag }, }.map { |(name, list)| Corpus.node(name, **list) } - HASH = Corpus.as_json - ANY_ARG_NAME = HASH[:args].pluck("name").map(&:to_s) + HASH = Corpus.as_json + ANY_ARG_NAME = HASH[:args].pluck("name").map(&:to_s) ANY_NODE_NAME = HASH[:nodes].pluck("name").map(&:to_s) - Corpus.enum(:LegalArgString, ANY_ARG_NAME, MISC_ENUM_ERR) + Corpus.enum(:LegalArgString, ANY_ARG_NAME, MISC_ENUM_ERR) Corpus.enum(:LegalKindString, ANY_NODE_NAME.map(&:camelize), MISC_ENUM_ERR) def self.no_resource(node, klass, resource_id) diff --git a/latest_corpus.rb b/latest_corpus.rb index 521c4f758..4bbe5f481 100755 --- a/latest_corpus.rb +++ b/latest_corpus.rb @@ -1,45 +1,47 @@ -WARNING_HEADER = \ -""" +WARNING_HEADER = + "" " // THIS INTERFACE WAS AUTO GENERATED ON #{Date.today} // DO NOT EDIT THIS FILE. // IT WILL BE OVERWRITTEN ON EVERY CELERYSCRIPT UPGRADE. -""" -HASH = Sequence::Corpus.as_json({}) -OUTPUT = [WARNING_HEADER] -FILE_PATH = "latest_corpus.ts" -VALUES = HASH.fetch(:values) -VALUE_PREFIX = "CS" -VALUES_TPL = "export type %{name} = %{type};\n" +" "" +HASH = Sequence::Corpus.as_json({}) +OUTPUT = [WARNING_HEADER] +FILE_PATH = "latest_corpus.ts" +VALUES = HASH.fetch(:values) +VALUE_PREFIX = "CS" +VALUES_TPL = "export type %{name} = %{type};\n" VALUES_OVERRIDE = HashWithIndifferentAccess.new(float: "number", integer: "number") # There are some rule exceptions when generating the Typescript corpus. -FUNNY_NAMES = { "Example" => "CSExample" } -ENUMS = HASH.fetch(:enums) -ENUM_TPL = "export type %{name} = %{type};\n" +FUNNY_NAMES = { "Example" => "CSExample" } +ENUMS = HASH.fetch(:enums) +ENUM_TPL = "export type %{name} = %{type};\n" ARGS = HASH .fetch(:args) .reduce(HashWithIndifferentAccess.new) do |acc, arg| - acc[arg.fetch("name").to_s] = arg - acc - end -NODES = HASH.fetch(:nodes) -NODE_START = [ "export type %{camel_case}BodyItem = %{body_types};", - "/** %{docs} %{tag_docs} */", - "export interface %{camel_case} {", - " comment?: string | undefined;", - ' kind: "%{snake_case}";', - " args: {", ].join("\n") -MIDDLE_CENTER = " %{arg_name}: %{arg_values};" -BOTTOM_END = [ " }", - " body?: %{camel_case}BodyItem[] | undefined;", - "}\n", ].join("\n") + acc[arg.fetch("name").to_s] = arg + acc +end +NODES = HASH.fetch(:nodes) +NODE_START = ["export type %{camel_case}BodyItem = %{body_types};", + "/** %{snake_case}\n%{docs}\n %{tag_docs} */", + "export interface %{camel_case} {", + " comment?: string | undefined;", + ' kind: "%{snake_case}";', + " args: {"].join("\n") +MIDDLE_CENTER = " %{arg_name}: %{arg_values};" +BOTTOM_END = [" }", + " body?: %{camel_case}BodyItem[] | undefined;", + "}\n"].join("\n") CONSTANT_DECLR_HACK = { LATEST_VERSION: Sequence::LATEST_VERSION, - DIGITAL: CeleryScriptSettingsBag::DIGITAL, - ANALOG: CeleryScriptSettingsBag::ANALOG, + DIGITAL: CeleryScriptSettingsBag::DIGITAL, + ANALOG: CeleryScriptSettingsBag::ANALOG, } CONSTANT_DECLR_HACK_TPL = "export const %{name} = %{value};\n" -PUBLIC_NODES = [] # Filled at runtime +PUBLIC_NODES = [] # Filled at runtime +PIPE = " |\n" + def emit_constants() CONSTANT_DECLR_HACK.map do |(name, value)| konst = CONSTANT_DECLR_HACK_TPL % { name: name, value: value } @@ -52,7 +54,7 @@ def add_to_output(string) end def save! - File.open(FILE_PATH, "w") { |f| f.write(OUTPUT.join("")) } + File.open(FILE_PATH, "w") { |f| f.write(OUTPUT.join("")) } puts "Saved to #{FILE_PATH}" end @@ -62,9 +64,9 @@ end def emit_values output = VALUES.map do |val| - real_name = name_of(val) - capitalized = real_name.capitalize - celerized = VALUE_PREFIX + capitalized + real_name = name_of(val) + capitalized = real_name.capitalize + celerized = VALUE_PREFIX + capitalized FUNNY_NAMES[capitalized] = celerized type = VALUES_OVERRIDE.fetch(real_name, real_name) VALUES_TPL % { name: celerized, type: type } @@ -77,53 +79,53 @@ end def emit_enums output = ENUMS.map do |enum| name = name_of(enum) - type = enum.fetch("allowed_values").sort.map(&:inspect).uniq.join(" | ") + type = enum.fetch("allowed_values").sort.map(&:inspect).uniq.join(PIPE) FUNNY_NAMES[name] = name ENUM_TPL % { name: name, type: type } end - .uniq - .sort + .uniq + .sort add_to_output(output) end def emit_nodes() nodes = NODES.map do |node| - tags = node.fetch("tags").sort.uniq - # Don't publish internal CeleryScript nodes: - next if tags.include?(:private) - tag_list = tags.join(", ") - name = name_of(node).to_s - bodies = node - .fetch("allowed_body_types") - .sort - .uniq - .map(&:to_s) - .map(&:camelize) - bt = bodies.any? ? "(#{bodies.join(" | ")})" : "never" - PUBLIC_NODES.push(name.camelize) - tpl_binding = { - body_types: bt, - camel_case: name.camelize, - docs: node.fetch("docs"), - snake_case: name, - tag_docs: "Tag properties: #{tag_list}." - } + tags = node.fetch("tags").sort.uniq + # Don't publish internal CeleryScript nodes: + next if tags.include?(:private) + tag_list = tags.join(", ") + name = name_of(node).to_s + bodies = node + .fetch("allowed_body_types") + .sort + .uniq + .map(&:to_s) + .map(&:camelize) + bt = bodies.any? ? "(#{bodies.join(PIPE)})" : "never" + PUBLIC_NODES.push(name.camelize) + tpl_binding = { + body_types: bt, + camel_case: name.camelize, + docs: node.fetch("docs"), + snake_case: name, + tag_docs: "Tag properties: #{tag_list}.", + } - one = NODE_START % tpl_binding - two = node.fetch("allowed_args").sort.map do |arg| - MIDDLE_CENTER % { - arg_name: arg.to_s, - arg_values: ARGS.fetch(arg) - .fetch("allowed_values") - .map(&:name) - .map { |x| FUNNY_NAMES[x] || x.camelize } - .join(" | ") - } - end - three = BOTTOM_END % tpl_binding - [one, two, three].flatten.join("\n") + one = NODE_START % tpl_binding + two = node.fetch("allowed_args").sort.map do |arg| + MIDDLE_CENTER % { + arg_name: arg.to_s, + arg_values: ARGS.fetch(arg) + .fetch("allowed_values") + .map(&:name) + .map { |x| FUNNY_NAMES[x] || x.camelize } + .join(PIPE), + } end + three = BOTTOM_END % tpl_binding + [one, two, three].flatten.join("\n") + end .compact .uniq .join("\n") @@ -131,13 +133,14 @@ def emit_nodes() end def emit_misc() - types = PUBLIC_NODES.sort.uniq.join(" | ") - tpl = "export type CeleryNode = #{types};\n" + types = PUBLIC_NODES.sort.uniq.join(PIPE) + tpl = "export type CeleryNode = #{types};\n" add_to_output(tpl) end + emit_constants() emit_values() emit_enums() emit_nodes() emit_misc() -save! \ No newline at end of file +save! diff --git a/spec/lib/celery_script/checker_spec.rb b/spec/lib/celery_script/checker_spec.rb index acd644178..8bf1ec4fc 100644 --- a/spec/lib/celery_script/checker_spec.rb +++ b/spec/lib/celery_script/checker_spec.rb @@ -1,4 +1,4 @@ -require 'spec_helper' +require "spec_helper" describe CeleryScript::Checker do let(:device) { FactoryBot.create(:device) } @@ -7,15 +7,15 @@ describe CeleryScript::Checker do kind: "sequence", args: { locals: Sequence::SCOPE_DECLARATION, - version: 0 + version: 0, }, comment: "Properly formatted, syntactically valid sequence.", - body: sequence_body_for(FakeSequence.create()) + body: sequence_body_for(FakeSequence.create()), }.deep_symbolize_keys end let(:tree) do - CeleryScript::AstNode.new(hash) + CeleryScript::AstNode.new(hash) end let (:corpus) { Sequence::Corpus } @@ -23,10 +23,10 @@ describe CeleryScript::Checker do let (:checker) { CeleryScript::Checker.new(tree, corpus, device) } it "runs through a syntactically valid program" do - outcome = checker.run! - expect(outcome).to be_kind_of(CeleryScript::AstNode) - expect(outcome.comment).to eq("Properly formatted, syntactically valid"\ - " sequence.") + outcome = checker.run! + expect(outcome).to be_kind_of(CeleryScript::AstNode) + expect(outcome.comment).to eq("Properly formatted, syntactically valid" \ + " sequence.") end it "handles missing args" do @@ -51,16 +51,16 @@ describe CeleryScript::Checker do end it "returns an error rather than raising one via #run()" do - outcome = checker.run - expect(outcome).to be_kind_of(CeleryScript::AstNode) - checker.tree.body.first.args[:x] = "No longer valid" - expect(checker.run).to be_kind_of(CeleryScript::TypeCheckError) + outcome = checker.run + expect(outcome).to be_kind_of(CeleryScript::AstNode) + checker.tree.body.first.args[:x] = "No longer valid" + expect(checker.run).to be_kind_of(CeleryScript::TypeCheckError) end - it 'handles wrong leaf types' do + it "handles wrong leaf types" do hash[:body][0][:args][:location][:args][:x] = "supposed to be an Integer" result = checker.run - expect(result.message).to eq("Expected leaf 'x' within 'coordinate' to be "\ + expect(result.message).to eq("Expected leaf 'x' within 'coordinate' to be " \ "one of: [Integer, Float] but got String") end @@ -76,10 +76,8 @@ describe CeleryScript::Checker do { kind: "execute", args: { sequence_id: 0 } }, ] chk = CeleryScript::Checker.new(tree, corpus, device) - expect(chk.valid?) - .to be false - expect(chk.error.message) - .to eq("missing a sequence selection for `execute` block.") + expect(chk.valid?).to be false + expect(chk.error.message).to eq("missing a sequence selection for `execute` block.") end it "validates peripheral presence" do @@ -91,13 +89,13 @@ describe CeleryScript::Checker do kind: "named_pin", args: { pin_type: "Peripheral", - pin_id: 0 - } + pin_id: 0, + }, }, pin_mode: CeleryScriptSettingsBag::ANALOG, - label: "FOO" - } - } + label: "FOO", + }, + }, ] chk = CeleryScript::Checker.new(tree, corpus, device) expect(chk.valid?).to be false @@ -109,14 +107,14 @@ describe CeleryScript::Checker do { kind: "read_pin", args: { - pin_mode: 0, - label: "pin", + pin_mode: 0, + label: "pin", pin_number: { kind: "named_pin", - args: { pin_type: "Not correct", pin_id: 1 } - } - } - } + args: { pin_type: "Not correct", pin_id: 1 }, + }, + }, + }, ] chk = CeleryScript::Checker.new(tree, corpus, device) expect(chk.valid?).to be false @@ -132,10 +130,10 @@ describe CeleryScript::Checker do label: "pin", pin_number: { kind: "named_pin", - args: { pin_type: "Peripheral", pin_id: 900 } - } - } - } + args: { pin_type: "Peripheral", pin_id: 900 }, + }, + }, + }, ] chk = CeleryScript::Checker.new(tree, corpus, device) expect(chk.valid?).to be false @@ -151,10 +149,10 @@ describe CeleryScript::Checker do pin_mode: 0, pin_number: { kind: "named_pin", - args: { pin_type: ["BoxLed3", "BoxLed4"].sample, pin_id: 41 } - } - } - } + args: { pin_type: ["BoxLed3", "BoxLed4"].sample, pin_id: 41 }, + }, + }, + }, ] chk = CeleryScript::Checker.new(tree, corpus, device) expect(chk.valid?).to be true @@ -169,17 +167,16 @@ describe CeleryScript::Checker do pin_mode: CeleryScriptSettingsBag::ANALOG, pin_number: { kind: "named_pin", - args: { pin_type: ["BoxLed3", "BoxLed4"].sample, pin_id: 41 } - } - } - } + args: { pin_type: ["BoxLed3", "BoxLed4"].sample, pin_id: 41 }, + }, + }, + }, ] chk = CeleryScript::Checker.new(tree, corpus, device) expect(chk.valid?).to be false expect(chk.error.message).to include(CeleryScriptSettingsBag::CANT_ANALOG) end - it 'gives human-friendly names to "BoxLed3", "BoxLed4"' do hash[:body] = [ { @@ -189,31 +186,29 @@ describe CeleryScript::Checker do pin_mode: CeleryScriptSettingsBag::DIGITAL, pin_number: { kind: "named_pin", - args: { pin_type: ["BoxLed3", "BoxLed4"].sample, pin_id: 0 } - } - } - } + args: { pin_type: ["BoxLed3", "BoxLed4"].sample, pin_id: 0 }, + }, + }, + }, ] chk = CeleryScript::Checker.new(tree, corpus, device) expect(chk.valid?).to be false - expected = \ + expected = CeleryScriptSettingsBag::NO_PIN_ID % CeleryScriptSettingsBag::BoxLed.name expect(chk.error.message).to eq(expected) end - it "catches bad `axis` nodes" do - t = \ - CeleryScript::AstNode.new({kind: "home", args: { speed: 100, axis: "?" }}) - chk = CeleryScript::Checker.new(t, corpus, device) + t = + CeleryScript::AstNode.new({ kind: "home", args: { speed: 100, axis: "?" } }) + chk = CeleryScript::Checker.new(t, corpus, device) expect(chk.valid?).to be false expect(chk.error.message).to include("not a valid axis") end it "catches bad `package` nodes" do - t = \ - CeleryScript::AstNode.new({ kind: "factory_reset", args: { package: "?" }}) - chk = CeleryScript::Checker.new(t, corpus, device) + t = CeleryScript::AstNode.new({ kind: "factory_reset", args: { package: "?" } }) + chk = CeleryScript::Checker.new(t, corpus, device) expect(chk.valid?).to be false expect(chk.error.message).to include("not a valid package") end @@ -225,7 +220,7 @@ describe CeleryScript::Checker do version: 20180209, locals: { kind: "scope_declaration", - :args=>{}, + :args => {}, body: [ { kind: "parameter_declaration", @@ -233,11 +228,12 @@ describe CeleryScript::Checker do label: "parent", default_value: { kind: "coordinate", - args: { x: 0, y: 0, z: 0 } } - } - } - ] - } + args: { x: 0, y: 0, z: 0 }, + }, + }, + }, + ], + }, }, body: [ { @@ -245,13 +241,13 @@ describe CeleryScript::Checker do args: { speed: 100, location: { kind: "identifier", args: { label: "parent" } }, - offset: { kind: "coordinate", args: { x: 0, y: 0, z: 0} } - } - } - ] + offset: { kind: "coordinate", args: { x: 0, y: 0, z: 0 } }, + }, + }, + ], } tree = CeleryScript::AstNode.new(ast) - chk = CeleryScript::Checker.new(tree, corpus, device) + chk = CeleryScript::Checker.new(tree, corpus, device) expect(chk.valid?).to be true end @@ -262,7 +258,7 @@ describe CeleryScript::Checker do version: 20180209, locals: { kind: "scope_declaration", - :args=>{}, + :args => {}, body: [ { kind: "parameter_declaration", @@ -270,12 +266,12 @@ describe CeleryScript::Checker do label: "parent", default_value: { kind: "nothing", - args: { } - } - } - } - ] - } + args: {}, + }, + }, + }, + ], + }, }, body: [ { @@ -283,13 +279,13 @@ describe CeleryScript::Checker do args: { speed: 100, location: { kind: "identifier", args: { label: "parent" } }, - offset: { kind: "coordinate", args: { x: 0, y: 0, z: 0} } - } - } - ] + offset: { kind: "coordinate", args: { x: 0, y: 0, z: 0 } }, + }, + }, + ], } tree = CeleryScript::AstNode.new(ast) - chk = CeleryScript::Checker.new(tree, corpus, device) + chk = CeleryScript::Checker.new(tree, corpus, device) expect(chk.valid?).to be false message = "must provide a value for all parameters" expect(chk.error.message).to include(message)