From 0ace75024505d8a315f00ade5772757f140ed8c7 Mon Sep 17 00:00:00 2001 From: JohnnyT Date: Sun, 26 Apr 2020 11:19:23 -0600 Subject: [PATCH 1/5] Adds Elixir to ASDF This adds Elixir 1.10.2-otp for ASDF to install and use. --- .tool-versions | 1 + 1 file changed, 1 insertion(+) diff --git a/.tool-versions b/.tool-versions index 405ff91..c5f50e9 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,3 @@ ruby 2.6.5 erlang 22.3 +elixir 1.10.2-otp-22 From 3f3c2ab523218cc2ae18a299b5320fd23a6b78ad Mon Sep 17 00:00:00 2001 From: JohnnyT Date: Sun, 26 Apr 2020 16:02:32 -0600 Subject: [PATCH 2/5] Ignores Elixir files from all subdirectories This applies the gitignore rules for *-ex/ to */ This now applies to the other subdirectories too. --- .gitignore | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 711fe72..765096c 100644 --- a/.gitignore +++ b/.gitignore @@ -45,19 +45,19 @@ Thumbs.db # Elixir # The directory Mix will write compiled artifacts to. -*-ex/_build/ +*/_build/ # If you run "mix test --cover", coverage assets end up here. -*-ex/cover/ +*/cover/ # The directory Mix downloads your dependencies sources to. -*-ex/deps/ +*/deps/ # Where 3rd-party dependencies like ExDoc output generated docs. -*-ex/doc/ +*/doc/ # Ignore .fetch files in case you like to edit your project deps locally. -*-ex/.fetch +*/.fetch # If the VM crashes, it generates a dump, let's ignore it too. erl_crash.dump @@ -69,12 +69,12 @@ erl_crash.dump npm-debug.log # The directory NPM downloads your dependencies sources to. -*-ex/assets/node_modules/ +*/assets/node_modules/ # Since we are building assets from assets/, # we ignore priv/static. You may want to comment # this depending on your deployment strategy. -*-ex/priv/static/ +*/priv/static/ # Files matching config/*.secret.exs pattern contain sensitive # data and you should not commit them into version control. @@ -82,4 +82,4 @@ npm-debug.log # Alternatively, you may comment the line below and commit the # secrets files as long as you replace their contents by environment # variables. -*-ex/config/*.secret.exs +*/config/*.secret.exs From 65608ee2a64b710b32d74ae7e42331ce38cfb0af Mon Sep 17 00:00:00 2001 From: JohnnyT Date: Sun, 26 Apr 2020 16:27:53 -0600 Subject: [PATCH 3/5] Ignores generated spec files This ignores files generated by the spec project. Once final, the generated files will be moved into each respective project. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 765096c..58f2f01 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ # Downloaded/installed as part of setup - OS specific bin/utils/oq +spec/generated # Compiled source *.com From a8f33d9c9460d7d825d313f571855635ccec433c Mon Sep 17 00:00:00 2001 From: JohnnyT Date: Sun, 26 Apr 2020 16:29:44 -0600 Subject: [PATCH 4/5] Sets up spec to generate files using Elixir This adds necessary dependencies to the Spec package to begin generating the language specs in Elixir using Liquid. --- bin/setup | 51 ++++++++++----- bin/spec | 11 ++++ spec/Makefile | 47 +++++++++++++ spec/generate.exs | 12 ++++ spec/mix.exs | 45 +++++++++++++ spec/mix.lock | 25 +++++++ spec/templates/ex/statechart_test.exs.liquid | 69 ++++++++++++++++++++ 7 files changed, 245 insertions(+), 15 deletions(-) create mode 100755 bin/spec create mode 100644 spec/Makefile create mode 100644 spec/generate.exs create mode 100644 spec/mix.exs create mode 100644 spec/mix.lock create mode 100644 spec/templates/ex/statechart_test.exs.liquid diff --git a/bin/setup b/bin/setup index 58f2471..ffb783c 100755 --- a/bin/setup +++ b/bin/setup @@ -18,37 +18,54 @@ export OQ_VERSION="1.0.2" # Setup ASDF version manager and install deps setup_asdf() { - _info_banner "[ASDF] Setting up" + local prefix="[ASDF]" + + _info_banner "${prefix} Setting up" # Version manager for multiple languages _prompt_for_command_install "asdf" _info_message "Installing/Updating ASDF dependencies" - (asdf install && _success_message "Success - ASDF dependencies up to date") || \ - _error_message "Failed to update ASDF dependencies" + (asdf install && _success_message "${prefix} Success: dependencies up to date") || \ + _error_message "${prefix} Failed to update dependencies" +} + +# Setup Ruby dependencies (used for Danger and generating templated specs) +setup_ruby() { + local prefix="[Ruby]" + + _info_banner "${prefix} Setting up" + + _info_message "${prefix} Installing/Updating RubyGems" + (bundle install && _success_message "${prefix} Success: RubyGems up to date") || \ + _error_message "${prefix} Failed to update RubyGems" } +_elixir_update() { + local subpath="${1?}" + local prefix="[${subpath}]" -# Setup dependencies for Elixir -setup_elixir() { - _info_banner "[Elixir] Setting up" - pushd statifier-ex 1>/dev/null + _info_banner "${prefix} Updating Elixir dependencies" + pushd "${subpath}" 1>/dev/null # shellcheck disable=SC1010 mix do deps.get, compile - popd 1>/dev/null - _success_message "[Elixir] All up to date!" + + _success_message "${prefix} Success: Elixir dependencies up to date" +} + +# Setup dependencies for statifier-ex +setup_statifier_ex() { + _elixir_update statifier-ex } # Setup dependencies for Spec setup_spec() { - _info_banner "[Spec] Setting up" + _elixir_update spec - # Working with YAML, XML and JSON files - _prompt_for_oq_install - - _success_message "[Spec] All up to date!" + # # Working with YAML, XML and JSON files + # _prompt_for_oq_install } @@ -56,9 +73,13 @@ setup_spec() { setup_project() { _info_banner "Setting up Statifier" + # Global Setup setup_asdf + setup_ruby + + # Package specific setup_spec - setup_elixir + setup_statifier_ex _success_banner "Project successfully setup" } diff --git a/bin/spec b/bin/spec new file mode 100755 index 0000000..feaed73 --- /dev/null +++ b/bin/spec @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -u # raise an error on use of uninitialized variable +set -e # exit after the first error + +# Wrapper around using the spec/Makefile using make v4 + +trap "popd 1>/dev/null" EXIT +pushd "spec" 1> /dev/null + +gmake "$@" diff --git a/spec/Makefile b/spec/Makefile new file mode 100644 index 0000000..0ef0c5f --- /dev/null +++ b/spec/Makefile @@ -0,0 +1,47 @@ +# Your Makefiles are wrong: https://tech.davis-hansson.com/p/make/ +# Self Documented Makefile: https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html +# Bash strict mode: http://redsymbol.net/articles/unofficial-bash-strict-mode/ + +SHELL := bash +.ONESHELL: +.SHELLFLAGS := -eu -o pipefail -c +.DELETE_ON_ERROR: +MAKEFLAGS += --warn-undefined-variables +MAKEFLAGS += --no-builtin-rules +HELP_TASK_WIDTH := 20 + +ifeq ($(origin .RECIPEPREFIX), undefined) + $(error This Make does not support .RECIPEPREFIX. Please use GNU Make 4.0 or later) +endif +.RECIPEPREFIX = > + +.DEFAULT_GOAL := help + +help: ## Displays help message describing each task +> @printf "\n%s\n\n" "Statifier Spec Make Tasks" +> @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-${HELP_TASK_WIDTH}s\033[0m %s\n", $$1, $$2}' +# Don't look for this on the filesystem +.PHONY: help + +generate: generated/.elixir ## Generate all specs +> @echo "Done!" +# Don't look for this on the filesystem +.PHONY: generate + +generated/.elixir: generated/ +> @mix run generate.exs elixir $@ + +generated/: +> @mkdir -p generated + +# w3c/.manifest: w3c/manifest.xml w3c_manifest.exs +# > @mix run w3c_manifest.exs w3c/manifest.xml $@ $(W3C_SUITE) + +clean: ## Cleanup (remove) all generated files. +> @rm -rf generated + +collect: $(shell find . -type d | grep '[0-9][0-9][0-9]' | awk '{printf $1 "/all.yml"}') ## Collect all sub specs into the **/*__all.yml file +> @echo +> @echo "hi" +# Don't look for this on the filesystem +.PHONY: help diff --git a/spec/generate.exs b/spec/generate.exs new file mode 100644 index 0000000..966017e --- /dev/null +++ b/spec/generate.exs @@ -0,0 +1,12 @@ +defmodule SpecGenerator do + def process(language, touchfile) do + full_touchfile = Path.expand(touchfile, ".") + + IO.puts("Hi from Elixir: #{language} #{full_touchfile}") + + File.touch!(full_touchfile) + end +end + +[language, touchfile] = System.argv() +SpecGenerator.process(language, touchfile) diff --git a/spec/mix.exs b/spec/mix.exs new file mode 100644 index 0000000..8209e71 --- /dev/null +++ b/spec/mix.exs @@ -0,0 +1,45 @@ +defmodule Statifier.MixProject do + use Mix.Project + + def project do + [ + app: :statifier_spec, + version: "0.1.0", + elixir: "~> 1.8", + start_permanent: Mix.env() == :prod, + docs: [extras: ["README.md"]], + description: description(), + # package: package(), + deps: deps() + ] + end + + def description, do: "Statifier Spec Generator" + + # def package do + # [ + # name: :statifier_spec, + # maintainers: ["JohnnyT"], + # licenses: ["MIT"], + # docs: [extras: ["README.md"]], + # links: %{"GitHub" => "https://github.com/riddler/statifier-ex"} + # ] + # end + + # # Run "mix help compile.app" to learn about applications. + # def application do + # [ + # extra_applications: [:logger] + # ] + # end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + {:credo, "~> 1.4", only: [:dev, :test], runtime: false}, + {:ex_doc, ">= 0.0.0", only: :dev}, + {:liquid, "~> 0.9"}, + {:yaml_elixir, "~> 2.4"} + ] + end +end diff --git a/spec/mix.lock b/spec/mix.lock new file mode 100644 index 0000000..50b1b24 --- /dev/null +++ b/spec/mix.lock @@ -0,0 +1,25 @@ +%{ + "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, + "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "805abd97539caf89ec6d4732c91e62ba9da0cda51ac462380bbd28ee697a8c42"}, + "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, + "credo": {:hex, :credo, "1.4.0", "92339d4cbadd1e88b5ee43d427b639b68a11071b6f73854e33638e30a0ea11f5", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1fd3b70dce216574ce3c18bdf510b57e7c4c85c2ec9cad4bff854abaf7e58658"}, + "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm", "8cf8a291ebf1c7b9539e3cddb19e9cef066c2441b1640f13c34c1d3cfc825fec"}, + "ex_doc": {:hex, :ex_doc, "0.21.3", "857ec876b35a587c5d9148a2512e952e24c24345552259464b98bfbb883c7b42", [:mix], [{:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "0db1ee8d1547ab4877c5b5dffc6604ef9454e189928d5ba8967d4a58a801f161"}, + "gettext": {:hex, :gettext, "0.17.4", "f13088e1ec10ce01665cf25f5ff779e7df3f2dc71b37084976cf89d1aa124d5c", [:mix], [], "hexpm", "3c75b5ea8288e2ee7ea503ff9e30dfe4d07ad3c054576a6e60040e79a801e14d"}, + "hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "e0100f8ef7d1124222c11ad362c857d3df7cb5f4204054f9f0f4a728666591fc"}, + "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "4bdd305eb64e18b0273864920695cb18d7a2021f31a11b9c5fbcd9a253f936e2"}, + "jason": {:hex, :jason, "1.2.0", "10043418c42d2493d0ee212d3fddd25d7ffe484380afad769a0a38795938e448", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "116747dbe057794c3a3e4e143b7c8390b29f634e16c78a7f59ba75bfa6852e7f"}, + "liquid": {:hex, :liquid, "0.9.1", "eef4797f4b8b1cdd14c44d6dcd52bb49df9caf6bcf5dacfd61911fba8b6a628d", [:mix], [{:timex, "~> 3.0", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "0472e078709036d0d31a82c6542d559cdad81552c40fb51745e108d4785db8df"}, + "makeup": {:hex, :makeup, "1.0.1", "82f332e461dc6c79dbd82fbe2a9c10d48ed07146f0a478286e590c83c52010b5", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "49736fe5b66a08d8575bf5321d716bac5da20c8e6b97714fec2bcd6febcfa1f8"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "d4b316c7222a85bbaa2fd7c6e90e37e953257ad196dc229505137c5e505e9eff"}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, + "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, + "nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"}, + "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm", "13104d7897e38ed7f044c4de953a6c28597d1c952075eb2e328bc6d6f2bfc496"}, + "timex": {:hex, :timex, "3.6.1", "efdf56d0e67a6b956cc57774353b0329c8ab7726766a11547e529357ffdc1d56", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "f354efb2400dd7a80fd9eb6c8419068c4f632da4ac47f3d8822d6e33f08bc852"}, + "tzdata": {:hex, :tzdata, "1.0.3", "73470ad29dde46e350c60a66e6b360d3b99d2d18b74c4c349dbebbc27a09a3eb", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a6e1ee7003c4d04ecbd21dd3ec690d4c6662db5d3bbdd7262d53cdf5e7c746c1"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm", "1d1848c40487cdb0b30e8ed975e34e025860c02e419cb615d255849f3427439d"}, + "yamerl": {:hex, :yamerl, "0.8.0", "8214cfe16bbabe5d1d6c14a14aea11c784b9a21903dd6a7c74f8ce180adae5c7", [:rebar3], [], "hexpm", "010634477bf9c208a0767dcca89116c2442cf0b5e87f9c870f85cd1c3e0c2aab"}, + "yaml_elixir": {:hex, :yaml_elixir, "2.4.0", "2f444abc3c994c902851fde56b6a9cb82895c291c05a0490a289035c2e62ae71", [:mix], [{:yamerl, "~> 0.7", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "4e25a6d5c873e393689c6f1062c5ec90f6cd1be2527b073178ae37eae4c78bee"}, +} diff --git a/spec/templates/ex/statechart_test.exs.liquid b/spec/templates/ex/statechart_test.exs.liquid new file mode 100644 index 0000000..f22a84e --- /dev/null +++ b/spec/templates/ex/statechart_test.exs.liquid @@ -0,0 +1,69 @@ +# AUTOGENERATED FILE - DO NOT EDIT + +defmodule Statifier.Spec.<%= spec.specid %>Test do + use ExUnit.Case, async: true + + @moduletag spec_id: "<%= spec.specid %>" + @moduletag spec_group: "<%= spec.specgroup %>" + @moduletag spec_subject: "Statechart" + @moduletag spec_description: """ + <%= spec.description %> + """ + @moduletag spec_statechart: <%= spec.statechart | elixir_map %> + + {% comment %} + alias Statifier.Statechart + + setup_all do + %{instructions: <%= instructions %>} + end +<% tests.each do |test| %> + test "<%= test["name"] %>", context do + predicate_context = <% if test["context"].nil? %>nil<% else %><%= elixir_hash test["context"] %><% end %> + expected_result = <% if test["result"].nil? %>nil<% else %><%= test["result"] %><% end %> + + result = Predicator.Evaluator.execute context[:instructions], predicate_context + assert expected_result == result + #assert_empty e.stack + end + {% endcomment %} +<% end %> +end +{% comment %} +defmodule Statifier.Spec. do + use ExUnit.Case + + alias Statifier.{Statechart, StateDef} + + # Definied in test helper + alias Statifier.Spec + + test "building basic statechart" do + test_path = Path.join(File.cwd!(), "test/fixtures/basic.yml") + + {:ok, test_config} = YamlElixir.read_from_file(test_path) + + sc = Statechart.build(test_config["statechart"]) + + assert %Statechart{ + name: "Valid Single State", + states: [%StateDef{id: "greeting"} | _rest] + } = sc + end + + # This corresponds to the element defined here: + # https://www.w3.org/TR/scxml/#scxml + + # A conformant SCXML document must have at least one , or + # child. + test "conformance: at least one state" do + spec = Spec.from_fixture("basic.yml") + + sc = + Statechart.build(spec.statechart) + |> Statechart.validate() + + assert %Statechart{conformant: true} = sc + end +end +{% endcomment %} From 12dee94374820daf538c763f3f4bd570f943043e Mon Sep 17 00:00:00 2001 From: JohnnyT Date: Wed, 15 Jul 2020 19:11:10 -0600 Subject: [PATCH 5/5] wip --- statifier-ex/test/statifier_spec.exs | 91 ++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 statifier-ex/test/statifier_spec.exs diff --git a/statifier-ex/test/statifier_spec.exs b/statifier-ex/test/statifier_spec.exs new file mode 100644 index 0000000..d27ae81 --- /dev/null +++ b/statifier-ex/test/statifier_spec.exs @@ -0,0 +1,91 @@ +defmodule Statifier.StatechartSpec do + @moduledoc """ + Implements and wraps a Statifier Statechart (SSC) specification. + + Example: + + ```yaml + --- + specid: SSC001_01 + description: > + # Statifier Statechart Spec 001_01 + + A conformant Statechart must have at least one state. + + [Update this to include more specifics about where they go] + + See: https://www.w3.org/TR/scxml/#scxml + + statechart: + name: Invalid Zero States + + statechart_tests: + - valid: false + errors: + - "The Statechart must have at least one state" + ``` + """ + + alias __MODULE__ + + @typedoc "String ID of spec (e.g. SSC001_01)" + @type specid :: String.t() + + @typedoc "Group ID of spec (e.g. SSC001). Allows for running all sub specs together." + @type specgroup :: String.t() + + @typedoc """ + Input for a statechart. Map keyed by strings. + """ + @type statechart :: Map.t(String.t(), any) + + @type statechart_test :: Map.t(String.t(), any) + + @type tests :: list(statechart_test) + + @type t :: %StatechartSpec{ + specid: specid, + description: String.t(), + statechart: statechart, + statechart_tests: tests + } + + defstruct specid: nil, + description: nil, + statechart: %{}, + statechart_tests: [] + + def from_fixture(fixture_path) when is_binary(fixture_path) do + "#{File.cwd!()}/test/fixtures" + |> Path.join(fixture_path) + |> from_file() + end + + def from_file(absolute_path) when is_binary(absolute_path) do + {:ok, spec_input} = YamlElixir.read_from_file(absolute_path) + build(spec_input) + end + + def build(input) when is_map(input) do + %Spec{} + |> put_statechart(input) + |> put_tests(input) + end + + # Incoming values will be keyed by strings not atoms + + defp put_specid(%Spec{} = spec, %{"specid" => specid}) + when is_binary(specid) do + %Spec{spec | specid: specid} + end + + defp put_statechart(%Spec{} = spec, %{"statechart" => statechart}) + when is_map(statechart) do + %Spec{spec | statechart: statechart} + end + + defp put_tests(%Spec{} = spec, %{"tests" => tests}) + when is_map(tests) do + %Spec{spec | tests: tests} + end +end