From b99a2d34d7e12a978690ecb18a22b3d88bf0d92c Mon Sep 17 00:00:00 2001 From: kelvinqian00 Date: Thu, 5 Oct 2023 10:17:32 -0400 Subject: [PATCH 01/11] Rework CLI input to put subcommands in front --- src/cli/com/yetanalytics/datasim/main.clj | 192 +++++++++++++++++++++- 1 file changed, 191 insertions(+), 1 deletion(-) diff --git a/src/cli/com/yetanalytics/datasim/main.clj b/src/cli/com/yetanalytics/datasim/main.clj index 54d4f11b..0165986b 100644 --- a/src/cli/com/yetanalytics/datasim/main.clj +++ b/src/cli/com/yetanalytics/datasim/main.clj @@ -26,6 +26,120 @@ [opt-map id v] (update-in opt-map [:parameters id] (fnil conj []) v)) +(def ^:private validate-input-options + [["-p" "--profile URI" "xAPI Profile Location" + :id :profiles + :desc "The location of an xAPI profile, can be used multiple times." + :parse-fn (partial input/from-location :profile :json) + :assoc-fn conj-input] + ["-a" "--actor-personae URI" "Actor Personae Location" + :id :personae-array + :desc "The location of an Actor Personae document indicating the actors in the sim." + :parse-fn (partial input/from-location :personae :json) + :assoc-fn conj-input] + ["-m" "--models URI" "Persona Model Location" + :id :models + :desc "The location of an Persona Model document, to describe alignments and overrides for the personae." + :parse-fn (partial input/from-location :models :json)] + ["-o" "--parameters URI" "Parameters Location" + :id :parameters + :desc "The location of simulation parameters document." + :parse-fn (partial input/from-location :parameters :json) + :default (params/apply-defaults)] + ["-i" "--input URI" "Pre-validated input location" + :id :input + :desc "The location of a JSON file containing a combined simulation input spec." + :parse-fn (partial input/from-location :input :json)] + ["-c" "--combined-input URI" "Validated combined input location" + :id :location + :desc "The location of the validated input to be produced"]]) + +(def ^:private input-options + [["-p" "--profile URI" "xAPI Profile Location" + :id :profiles + :desc "The location of an xAPI profile, can be used multiple times." + :parse-fn (partial input/from-location :profile :json) + :validate [(partial input/validate-throw :profile) + "Failed to validate profile."] + :assoc-fn conj-input] + ["-a" "--actor-personae URI" "Actor Personae Location" + :id :personae-array + :desc "The location of an Actor Personae document indicating the actors in the sim." + :parse-fn (partial input/from-location :personae :json) + :validate [(partial input/validate-throw :personae) + "Failed to validate personae."] + :assoc-fn conj-input] + ["-m" "--models URI" "Persona Model Location" + :id :models + :desc "The location of an Persona Model document, to describe alignments and overrides for the personae." + :parse-fn (partial input/from-location :models :json) + :validate [(partial input/validate-throw :models) + "Failed to validate Models."]] + ["-o" "--parameters URI" "Parameters Location" + :id :parameters + :desc "The location of simulation parameters document." + :parse-fn (partial input/from-location :parameters :json) + :validate [(partial input/validate-throw :parameters) + "Failed to validate Parameters."] + :default (params/apply-defaults)] + ["-i" "--input URI" "Pre-validated input location" + :id :input + :desc "The location of a JSON file containing a combined simulation input spec." + :parse-fn (partial input/from-location :input :json) + :validate [(partial input/validate-throw :input) + "Failed to validate input."]]]) + +(def ^:private generate-options + [[nil "--seed SEED" "Override input seed" + :id :override-seed + :parse-fn parse-long + :validate [int? "Seed is not an integer."] + :desc "An integer seed to override the one in the input spec. Use -1 for random."] + [nil "--actor AGENT_ID" "Select actor(s) by agent ID" + :id :select-agents + :multi true + :update-fn (fnil conj #{}) + :desc "Pass an agent id in the format mbox::malto:bob@example.org to select actor(s)"] + [nil "--gen-profile IRI" "Only generate based on primary patterns in the given profile. May be given multiple times to include multiple profiles." + :id :gen-profiles + :assoc-fn conj-param-input] + [nil "--gen-pattern IRI" "Only generate based on the given primary pattern. May be given multiple times to include multiple patterns." + :id :gen-patterns + :assoc-fn conj-param-input]]) + +(def ^:private post-options + [["-E" "--endpoint URI" "LRS Endpoint for POST" + :id :endpoint + :desc "The xAPI endpoint of an LRS to POST to, ex: https://lrs.example.org/xapi" + :missing "[-E|--endpoint] argument is required for POST."] + ["-U" "--username URI" "LRS Basic auth username" + :id :username + :desc "The basic auth username for the LRS you wish to post to"] + ["-P" "--password URI" "LRS Basic auth password" + :id :password + :desc "The basic auth password for the LRS you wish to post to"] + ["-B" "--batch-size SIZE" "LRS POST batch size" + :id :batch-size + :default 25 + :parse-fn parse-long + :validate [int? "Batch size is not an integer."] + :desc "The batch size for POSTing to an LRS"] + ["-C" "--concurrency CONC" "LRS POST concurrency" + :id :concurrency + :default 4 + :parse-fn parse-long + :validate [int? "Concurrency is not an integer."] + :desc "The max concurrency of the LRS POST pipeline"] + ["-L" "--post-limit LIMIT" "LRS POST total statement limit" + :id :post-limit + :default 999 + :parse-fn parse-long + :validate [int? "POST statement limit is not an integer."] + :desc "The total number of statements that will be sent to the LRS before termination. Overrides sim params. Set to -1 for no limit."] + ["-A" "--[no-]async" "Async operation. Use --no-async if statements must be sent to server in timestamp order." + :id :async + :default true]]) + (defn cli-options "Generate CLI options, skipping validation if `validate?` is false" [validate?] @@ -237,8 +351,84 @@ ;; Main function ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(def top-level-options + [["-h" "--help" "Display the top-level help guide."]]) + +(def top-level-summary + (str "Usage: 'datasim ' or 'datasim [-h|--help]'.\n" + "\n" + " where the subcommand can be one of the following:\n" + " validate-input: Validate the input and create an input JSON file.\n" + " generate: Generate statements from input and print to stdout.\n" + " generate-post: Generate statements from input and POST them to an LRS.\n" + "\n" + " Run 'datasim --help' for more info on each subcommand.")) + +(defn- exec-subcommand [cli-options exec-fn args] + (let [{:keys [options summary errors]} + (cli/parse-opts args cli-options) + {:keys [help]} + options + errors* (not-empty errors)] + (cond + help (println summary) + errors* (bail! errors*) + :else (exec-fn options)))) + +(defn- validate-input [args] + (exec-subcommand + (conj validate-input-options + ["-h" "--help"]) + (fn [{:keys [location] :as options}] + (let [input (sim-input options)] + (assert-valid-input input) + (if location + (write-input! input location) + (write-input! input)))) + args)) + +(defn- generate [args] + (exec-subcommand + (concat input-options + generate-options + [["-h" "--help"]]) + (fn [options] + (let [input (sim-input options)] + (assert-valid-input input) + (print-sim! input options))) + args)) + +(defn- generate-post [args] + (exec-subcommand + (concat input-options + generate-options + post-options + [["-h" "--help"]]) + (fn [options] + (let [input (sim-input options)] + (assert-valid-input input) + (post-sim! input options))) + args)) + (defn -main [& args] - (let [;; If the verb is "validate-input", we + (let [{:keys [options arguments summary errors]} + (cli/parse-opts args top-level-options + :in-order true + :summary-fn (fn [_] top-level-summary)) + [subcommand & rest-args] + arguments] + (cond + (:help options) + (println summary) + (not subcommand) + (print "No subcommand entered.\n\n" summary) + :else + (case subcommand + "validate-input" (validate-input rest-args) + "generate" (generate rest-args) + "generate-post" (generate-post rest-args) + (bail! errors)))) + #_(let [;; If the verb is "validate-input", we ;; skip tools.cli validation and do a ;; more in-depth one. cli-opts From b585826c390b918e835b74d40b5e50e2785b0fd4 Mon Sep 17 00:00:00 2001 From: kelvinqian00 Date: Thu, 5 Oct 2023 10:18:08 -0400 Subject: [PATCH 02/11] Update CLI commands in Makefile --- Makefile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 68eaea5e..2ca3af21 100644 --- a/Makefile +++ b/Makefile @@ -44,16 +44,16 @@ test-unit-onyx: clojure -Adev:cli:onyx:run-onyx-tests test-cli: - clojure -A:cli:run -p dev-resources/profiles/cmi5/fixed.json -a dev-resources/personae/simple.json -m dev-resources/models/simple.json -o dev-resources/parameters/simple.json validate-input dev-resources/input/simple.json + clojure -A:cli:run validate-input -p dev-resources/profiles/cmi5/fixed.json -a dev-resources/personae/simple.json -m dev-resources/models/simple.json -o dev-resources/parameters/simple.json -c dev-resources/input/simple.json test-cli-comprehensive: - clojure -A:cli:run -i dev-resources/input/simple.json validate-input dev-resources/input/simple.json + clojure -A:cli:run validate-input -i dev-resources/input/simple.json -c dev-resources/input/simple.json test-cli-output: - clojure -A:cli:run -i dev-resources/input/simple.json generate + clojure -A:cli:run generate -i dev-resources/input/simple.json test-bundle-output: bundle - cd target/bundle; bin/run.sh -i ../../dev-resources/input/simple.json generate + cd target/bundle; bin/run.sh generate -i ../../dev-resources/input/simple.json validate-template: AWS_PAGER="" aws cloudformation validate-template --template-body file://template/0_vpc.yml From bc0d356e59a61d02adb0908faaf6cf13e0990169 Mon Sep 17 00:00:00 2001 From: kelvinqian00 Date: Thu, 5 Oct 2023 11:01:06 -0400 Subject: [PATCH 03/11] Update CLI in README (+ update some descriptions) --- README.md | 111 +++++++++++++--------- src/cli/com/yetanalytics/datasim/main.clj | 22 ++--- 2 files changed, 79 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index adc82661..f1ac524f 100644 --- a/README.md +++ b/README.md @@ -181,55 +181,80 @@ In the form of a CLI application, DATASIM takes the inputs listed above as JSON For the CLI the first step is to build the project so that it can be run on a JVM. +``` make bundle +``` Now that we have this, navigate to target/bundle and run +``` bin/run.sh +``` + +With no commands or `--help` it will give you the list of subcommands: + +| Subcommand | Description +| --- | --- +| `validate-input` | Validate the input and create an input JSON file. +| `generate` | Generate statements from input and print to stdout. +| `generate-post` | Generate statements from input and POST them to an LRS. + +The `validate-input` subcommand is used to validate and combine input files. These are its arguments: + +| Argument | Description +| --- | --- +| `-p, --profile URI` | The location of an xAPI profile, can be used multiple times. +| `-a, --actor-personae URI` | The location of an Actor Personae document indicating the actors in the sim. +| `-m, --models URI` | The location of an Persona Model document, to describe alignments and overrides for the personae. +| `-o, -parameters URI` | The location of simulation parameters document. (The "o" stands for "options.") +| `-i, --input URI` | The location of a JSON file containing a combined simulation input spec. +| `-c, --combined-input URI` | The location of the validated input to be produced. + +The `generate` subcommand is used to generate statements from an input and print them to standard output. The inputs can be a combined `--input` location or a combination of `-p`, `-a`, `-m`, and `-o` inputs. The additional arguments are as follows: +| Argument | Description +| --- | --- +| `--seed SEED` | An integer seed to override the one in the input spec. Use -1 for a random seed. +| `--actor AGENT_ID` | Pass an agent id in the format 'mbox::mailto:[email]' to select actor(s) +| `--gen-profile IRI` | Only generate based on primary patterns in the given profile. May be given multiple times to include multiple profiles. +| `--gen-pattern IRI` | Only generate based on the given primary pattern. May be given multiple times to include multiple patterns. + +The `generate-post` subcommand is used to generate statements from an input and POST them to an LRS. In addition to the `generate` arguments, this subcommands has these additional arguments: +| Argument | Description +| --- | --- +| `-E, --endpoint URI` | The xAPI endpoint of an LRS to POST to, ex: `https://lrs.example.org/xapi` +| `-U, --username URI` | The Basic Auth username for the LRS. +| `-P, --password URI` | The Basic Auth password for the LRS. +| `-B, --batch-size SIZE` | The batch size, i.e. how many statements to send at a time, for POSTing. +| `-C, --concurrency CONC` | The max concurrency of the LRS POST pipeline. +| `-L, --post-limit LIMIT` | The total number of statements that will be sent to the LRS before termination. Overrides sim params. Set to -1 for no limit. +| `-A, --[no-]async` | Async operation. Use `--no-async`` if statements must be sent to server in timestamp order. + +The following is an example of a simple run. We first create a combined input file using `validate-input`: +``` +bin/run.sh validate-input \ + -p dev-resources/profile/cmi5/fixed.json \ + -a dev-resources/personae/simple.json \ + -m dev-resources/models/simple.json \ + -o dev-resources/parameters/simple.json \ + -c dev-resources/input/simple.json +``` -With no commands or `--help` it will give you the list of parameters: - - -p, --profile URI The location of an xAPI profile, can be used multiple times. - -a, --actor-personae URI The location of an Actor Personae document indicating the actors in the sim, can be used multiple times. - -m, --models URI The location of an Personae Model Document. - -o, --parameters URI {...} The location of a Sim Parameters Document. - -i, --input URI The location of a JSON file containing a combined simulation input spec. - --seed SEED An integer seed to override the one in the input spec. Use -1 for random. - --actor AGENT_ID Pass an agent id in the format mbox::malto:bob@example.org to select actor(s) - -E, --endpoint URI The xAPI endpoint of an LRS to POST to, ex: https://lrs.example.org/xapi - -U, --username URI The basic auth username for the LRS you wish to post to - -P, --password URI The basic auth password for the LRS you wish to post to - -B, --batch-size SIZE 25 The batch size for POSTing to an LRS - -C, --concurrency CONC 4 The max concurrency of the LRS POST pipeline - -L, --post-limit LIMIT 999 The total number of statements that will be sent to the LRS before termination. Overrides sim params. Set to -1 for no limit. - -A, --[no-]async Async operation. Use --no-async if statements must be sent to server in timestamp order. - --gen-profile IRI Only generate based on primary patterns in the given profile. May be given multiple times to include multiple profiles. - --gen-pattern IRI Only generate based on the given primary pattern. May be given multiple times to include multiple patterns. - -h, --help Show this list. - -For a simple run, we will first create the simulation specification by combining the inputs, validating them, and outputting to a simulation input file like so: - - bin/run.sh -p [profile json file] \ - -a [actors json filename] \ - -m [models json filename] \ - -o [sim params json filename] \ - validate-input [desired output filename] - -Once we have that simulation specification, we can run the sim just from that like so: - - bin/run.sh -i dev-resources/input/simple.json generate - -###### CLI LRS POST - -If we have an endpoint and credentials for an LRS we can direcly POST the statements to it: - - bin/run.sh -i dev-resources/input/simple.json \ - -E [LRS xAPI endpoint ex. https://lrs.example.org/xapi] \ - -U [basic auth username] \ - -P [basic auth password] \ - -B [batch size] \ - -L [limit statements posted, -1 is no limit] \ - generate post +Once we have that sim specification, we can run the simulation using the `generate`: +``` +bin/run.sh generate -i dev-resources/input/simple.json +``` + +If we have an endpoint and credentials for an LRS we can directly POST the simulated statements using `generate-post`: + +``` +bin/run.sh generate-post \ + -i dev-resources/input/simple.json \ + -E http://localhost:8080/xapi \ + -U username \ + -P password \ + -B 20 \ + -L 1000 \ +``` As statements are successfully sent to the LRS their IDs will be sent to stdout. diff --git a/src/cli/com/yetanalytics/datasim/main.clj b/src/cli/com/yetanalytics/datasim/main.clj index 0165986b..2512152d 100644 --- a/src/cli/com/yetanalytics/datasim/main.clj +++ b/src/cli/com/yetanalytics/datasim/main.clj @@ -52,7 +52,7 @@ :parse-fn (partial input/from-location :input :json)] ["-c" "--combined-input URI" "Validated combined input location" :id :location - :desc "The location of the validated input to be produced"]]) + :desc "The location of the validated input to be produced."]]) (def ^:private input-options [["-p" "--profile URI" "xAPI Profile Location" @@ -99,7 +99,7 @@ :id :select-agents :multi true :update-fn (fnil conj #{}) - :desc "Pass an agent id in the format mbox::malto:bob@example.org to select actor(s)"] + :desc "Pass an agent id in the format 'mbox::malito:[email]' to select actor(s)"] [nil "--gen-profile IRI" "Only generate based on primary patterns in the given profile. May be given multiple times to include multiple profiles." :id :gen-profiles :assoc-fn conj-param-input] @@ -114,22 +114,22 @@ :missing "[-E|--endpoint] argument is required for POST."] ["-U" "--username URI" "LRS Basic auth username" :id :username - :desc "The basic auth username for the LRS you wish to post to"] + :desc "The Basic Auth username for the LRS."] ["-P" "--password URI" "LRS Basic auth password" :id :password - :desc "The basic auth password for the LRS you wish to post to"] + :desc "The Basic Auth password for the LRS."] ["-B" "--batch-size SIZE" "LRS POST batch size" :id :batch-size :default 25 :parse-fn parse-long :validate [int? "Batch size is not an integer."] - :desc "The batch size for POSTing to an LRS"] + :desc "The batch size, i.e. how many statements to send at a time, for POSTing."] ["-C" "--concurrency CONC" "LRS POST concurrency" :id :concurrency :default 4 :parse-fn parse-long :validate [int? "Concurrency is not an integer."] - :desc "The max concurrency of the LRS POST pipeline"] + :desc "The max concurrency of the LRS POST pipeline."] ["-L" "--post-limit LIMIT" "LRS POST total statement limit" :id :post-limit :default 999 @@ -357,12 +357,12 @@ (def top-level-summary (str "Usage: 'datasim ' or 'datasim [-h|--help]'.\n" "\n" - " where the subcommand can be one of the following:\n" - " validate-input: Validate the input and create an input JSON file.\n" - " generate: Generate statements from input and print to stdout.\n" - " generate-post: Generate statements from input and POST them to an LRS.\n" + "where the subcommand can be one of the following:\n" + " validate-input: Validate the input and create an input JSON file.\n" + " generate: Generate statements from input and print to stdout.\n" + " generate-post: Generate statements from input and POST them to an LRS.\n" "\n" - " Run 'datasim --help' for more info on each subcommand.")) + "Run 'datasim --help' for more info on each subcommand.")) (defn- exec-subcommand [cli-options exec-fn args] (let [{:keys [options summary errors]} From 1a0ffc6602504c2d526342bb3a66bd4a32a18cb4 Mon Sep 17 00:00:00 2001 From: kelvinqian00 Date: Thu, 5 Oct 2023 11:39:22 -0400 Subject: [PATCH 04/11] Split cli namespace (+ refactor out descs) --- .../com/yetanalytics/datasim/cli/generate.clj | 201 ++++++++ .../com/yetanalytics/datasim/cli/input.clj | 154 ++++++ src/cli/com/yetanalytics/datasim/cli/util.clj | 36 ++ src/cli/com/yetanalytics/datasim/main.clj | 442 +----------------- 4 files changed, 399 insertions(+), 434 deletions(-) create mode 100644 src/cli/com/yetanalytics/datasim/cli/generate.clj create mode 100644 src/cli/com/yetanalytics/datasim/cli/input.clj create mode 100644 src/cli/com/yetanalytics/datasim/cli/util.clj diff --git a/src/cli/com/yetanalytics/datasim/cli/generate.clj b/src/cli/com/yetanalytics/datasim/cli/generate.clj new file mode 100644 index 00000000..2077773c --- /dev/null +++ b/src/cli/com/yetanalytics/datasim/cli/generate.clj @@ -0,0 +1,201 @@ +(ns com.yetanalytics.datasim.cli.generate + "CLI options and functions for statement generation (including + statement POSTing)." + (:require [clojure.core.async :as a] + [com.yetanalytics.datasim :as ds] + [com.yetanalytics.datasim.cli.util :as u] + [com.yetanalytics.datasim.cli.input :as cli-input] + [com.yetanalytics.datasim.client :as client] + [com.yetanalytics.datasim.util.io :as dio])) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; CLI Input Options +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(def seed-desc + "An integer seed to override the one in the input spec. Use -1 for random.") + +(def select-agent-desc + "Pass an agent IFI in the format 'mbox::malito:[email]' to select actor(s)") + +(def gen-profiles-desc + "Only generate based on primary patterns in the given profile. May be given multiple times to include multiple profiles.") + +(def gen-patterns-desc + "Only generate based on the given primary pattern. May be given multiple times to include multiple patterns.") + +(def generate-options + [[nil "--seed SEED" "Input seed" + :id :override-seed + :parse-fn parse-long + :validate [int? "Seed is not an integer."] + :desc seed-desc] + [nil "--actor AGENT_IFI" "Selected Actor IFIs" + :id :select-agents + :multi true + :update-fn (fnil conj #{}) + :desc select-agent-desc] + [nil "--gen-profile IRI" "Select Profile IRIs" + :id :gen-profiles + :assoc-fn u/conj-param-input + :desc gen-profiles-desc] + [nil "--gen-pattern IRI" "Select Pattern IRIs" + :id :gen-patterns + :assoc-fn u/conj-param-input + :desc gen-patterns-desc]]) + +(def endpoint-desc + "The xAPI endpoint of an LRS to POST to, ex: https://lrs.example.org/xapi") + +(def endpoint-missing + "[-E|--endpoint] argument is required for POST.") + +(def username-desc + "The Basic Auth username for the LRS.") + +(def password-desc + "The Basic Auth password for the LRS.") + +(def batch-size-desc + "The batch size, i.e. how many statements to send at a time, for POSTing.") + +(def concurrency-desc + "The max concurrency of the LRS POST pipeline.") + +(def post-limit-desc + "The total number of statements that will be sent to the LRS before termination. Overrides sim params. Set to -1 for no limit.") + +(def post-options + [["-E" "--endpoint URI" "LRS Endpoint for POST" + :id :endpoint + :desc endpoint-desc + :missing endpoint-missing] + ["-U" "--username URI" "LRS username" + :id :username + :desc username-desc] + ["-P" "--password URI" "LRS password" + :id :password + :desc password-desc] + ["-B" "--batch-size SIZE" "LRS POST batch size" + :id :batch-size + :default 25 + :parse-fn parse-long + :validate [int? "Batch size is not an integer."] + :desc batch-size-desc] + ["-C" "--concurrency CONC" "LRS POST concurrency" + :id :concurrency + :default 4 + :parse-fn parse-long + :validate [int? "Concurrency is not an integer."] + :desc concurrency-desc] + ["-L" "--post-limit LIMIT" "LRS POST total statement limit" + :id :post-limit + :default 999 + :parse-fn parse-long + :validate [int? "POST statement limit is not an integer."] + :desc post-limit-desc] + ["-A" "--[no-]async" "Async operation. Use --no-async if statements must be sent to server in timestamp order." + :id :async + :default true]]) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Generate Functions +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn- print-sim! + [input {:keys [select-agents]}] + (doseq [statement (ds/generate-seq input :select-agents select-agents)] + (dio/write-json-stdout statement :key-fn? false))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Generate POST Functions +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn- post-async! + [input post-options post-limit select-agents concurrency] + (let [gen-input (cond-> input + ;; when async, we just use the post + ;; limit as the max + (not= post-limit -1) + (assoc-in [:parameters :max] post-limit)) + sim-chan (ds/generate-seq-async + gen-input + :select-agents select-agents) + result-chan (client/post-statements-async + post-options + sim-chan + :concurrency concurrency)] + (loop [] + (when-let [[tag ret] (a/> (ds/generate-seq + input + :select-agents select-agents) + (not= post-limit -1) + (take post-limit)) + {:keys [fail]} (client/post-statements post-options statements)] + (when (not-empty fail) + (u/bail! (for [{:keys [status error]} fail] + (client/post-error-message status error)))))) + +(defn- post-sim! + [input options] + (let [{:keys [endpoint + username + password + batch-size + concurrency + post-limit + select-agents + async]} + options + post-options + {:endpoint endpoint + :batch-size batch-size + :username username + :password password}] + (if async + (post-async! input post-options post-limit select-agents concurrency) + (post-sync! input post-options post-limit select-agents)))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Subcommands +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn generate + "Generate statements based on simulation `args` and print them to stdout." + [args] + (u/exec-subcommand + (concat cli-input/input-options + generate-options + [["-h" "--help"]]) + (fn [options] + (let [input (cli-input/sim-input options)] + (cli-input/assert-valid-input input) + (print-sim! input options))) + args)) + +(defn generate-post + "Generate statements based on simulation `args` and POST them to an LRS + (whose endpoint and other properties are also in `args`)." + [args] + (u/exec-subcommand + (concat cli-input/input-options + generate-options + post-options + [["-h" "--help"]]) + (fn [options] + (let [input (cli-input/sim-input options)] + (cli-input/assert-valid-input input) + (post-sim! input options))) + args)) diff --git a/src/cli/com/yetanalytics/datasim/cli/input.clj b/src/cli/com/yetanalytics/datasim/cli/input.clj new file mode 100644 index 00000000..ba7bf6ce --- /dev/null +++ b/src/cli/com/yetanalytics/datasim/cli/input.clj @@ -0,0 +1,154 @@ +(ns com.yetanalytics.datasim.cli.input + "CLI options and functions for sim inputs (including input validation)." + (:require [com.yetanalytics.datasim.cli.util :as u] + [com.yetanalytics.datasim.input :as input] + [com.yetanalytics.datasim.input.parameters :as params] + [com.yetanalytics.datasim.math.random :as random] + [com.yetanalytics.datasim.util.errors :as errors])) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; CLI Input Options +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(def profiles-desc + "The location of an xAPI profile, can be used multiple times.") + +(def personae-array-desc + "The location of an Actor Personae document indicating the actors in the sim.") + +(def models-desc + "The location of an Persona Model document, to describe alignments and overrides for the personae.") + +(def parameters-desc + "The location of simulation parameters document.") + +(def input-desc + "The location of a JSON file containing a combined simulation input spec.") + +(def combined-input-desc + "The location of the validated input to be produced.") + +;; NOTE: For the `validate-input` subcommand, we skip tools.cli validation and +;; do more in-depth validation involving combined inputs. +(def validate-input-options + [["-p" "--profile URI" "xAPI Profile Location" + :id :profiles + :desc profiles-desc + :parse-fn (partial input/from-location :profile :json) + :assoc-fn u/conj-input] + ["-a" "--actor-personae URI" "Actor Personae Location" + :id :personae-array + :desc personae-array-desc + :parse-fn (partial input/from-location :personae :json) + :assoc-fn u/conj-input] + ["-m" "--models URI" "Persona Model Location" + :id :models + :desc models-desc + :parse-fn (partial input/from-location :models :json)] + ["-o" "--parameters URI" "Parameters Location" + :id :parameters + :desc parameters-desc + :parse-fn (partial input/from-location :parameters :json) + :default (params/apply-defaults)] + ["-i" "--input URI" "Pre-validated input location" + :id :input + :desc input-desc + :parse-fn (partial input/from-location :input :json)] + ["-c" "--combined-input URI" "Validated combined input location" + :id :location + :desc combined-input-desc]]) + +(def input-options + [["-p" "--profile URI" "xAPI Profile Location" + :id :profiles + :desc profiles-desc + :parse-fn (partial input/from-location :profile :json) + :validate [(partial input/validate-throw :profile) + "Failed to validate profile."] + :assoc-fn u/conj-input] + ["-a" "--actor-personae URI" "Actor Personae Location" + :id :personae-array + :desc personae-array-desc + :parse-fn (partial input/from-location :personae :json) + :validate [(partial input/validate-throw :personae) + "Failed to validate personae."] + :assoc-fn u/conj-input] + ["-m" "--models URI" "Persona Model Location" + :id :models + :desc models-desc + :parse-fn (partial input/from-location :models :json) + :validate [(partial input/validate-throw :models) + "Failed to validate Models."]] + ["-o" "--parameters URI" "Parameters Location" + :id :parameters + :desc parameters-desc + :parse-fn (partial input/from-location :parameters :json) + :validate [(partial input/validate-throw :parameters) + "Failed to validate Parameters."] + :default (params/apply-defaults)] + ["-i" "--input URI" "Pre-validated input location" + :id :input + :desc input-desc + :parse-fn (partial input/from-location :input :json) + :validate [(partial input/validate-throw :input) + "Failed to validate input."]]]) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Helper Functions +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn sim-input + "Given `options`, return a map of just the simulation options + (either just `:input`, or a combo `:profiles`, `:personae-array`, + `:parameters`, and `:models`)." + [options] + (let [sim-options (select-keys options [:input + :profiles + :personae-array + :parameters + :models]) + {:keys [override-seed]} options] + (cond-> (or (:input sim-options) + (dissoc sim-options :input)) + override-seed + (assoc-in [:parameters :seed] + (if (= -1 override-seed) + (random/rand-unbound-int (random/rng)) + override-seed))))) + +(defn assert-valid-input + "Perform validation on `input` and fail w/ early termination if + it is not valid. + + When this is called, we should have valid individual inputs. However, there + may be cross-validation that needs to happen, so we compose the + comprehensive spec from the options and check that." + [input] + (when-let [errors (not-empty (input/validate :input input))] + (u/bail! (errors/map-coll->strs errors)))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Subcommand +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn- write-input! + ([input] + (input/to-out input :json)) + ([input location] + (input/to-file input :json location) + (println (format "Input specification written to %s" location)))) + +(defn validate-input + "Combine and validate the arguments given in `args` and write them + to `location` (if `location` is provided)." + [args] + (u/exec-subcommand + (conj validate-input-options + ["-h" "--help"]) + (fn [{:keys [location] :as options}] + (let [input (sim-input options)] + (assert-valid-input input) + (if location + (write-input! input location) + (write-input! input)))) + args)) diff --git a/src/cli/com/yetanalytics/datasim/cli/util.clj b/src/cli/com/yetanalytics/datasim/cli/util.clj new file mode 100644 index 00000000..5c60b463 --- /dev/null +++ b/src/cli/com/yetanalytics/datasim/cli/util.clj @@ -0,0 +1,36 @@ +(ns com.yetanalytics.datasim.cli.util + (:require [clojure.tools.cli :as cli] + [com.yetanalytics.datasim.util.io :as dio])) + +(defn conj-input + "Conj the input (either a Profile or a personae) to return a + vector of inputs, e.g. `-p profile-1 -p profile-2` becomes + `[profile-1 profile-2]`." + [opt-map id v] + (update opt-map id (fnil conj []) v)) + +(defn conj-param-input + "Add a parameter named by id." + [opt-map id v] + (update-in opt-map [:parameters id] (fnil conj []) v)) + +(defn bail! + "Print error messages to standard error and exit." + [errors & {:keys [status] + :or {status 1}}] + (dio/println-err-coll errors) + (System/exit status)) + +(defn exec-subcommand + "Execute `exec-fn` for a subcommand with arguments `args`, where the + valid options are `cli-options`." + [cli-options exec-fn args] + (let [{:keys [options summary errors]} + (cli/parse-opts args cli-options) + {:keys [help]} + options + errors* (not-empty errors)] + (cond + help (println summary) + errors* (bail! errors*) + :else (exec-fn options)))) diff --git a/src/cli/com/yetanalytics/datasim/main.clj b/src/cli/com/yetanalytics/datasim/main.clj index 2512152d..f3cbf972 100644 --- a/src/cli/com/yetanalytics/datasim/main.clj +++ b/src/cli/com/yetanalytics/datasim/main.clj @@ -1,356 +1,10 @@ (ns com.yetanalytics.datasim.main - (:require [clojure.core.async :as a] - [clojure.tools.cli :as cli] - [com.yetanalytics.datasim :as ds] - [com.yetanalytics.datasim.client :as client] - [com.yetanalytics.datasim.input :as input] - [com.yetanalytics.datasim.input.parameters :as params] - [com.yetanalytics.datasim.math.random :as random] - [com.yetanalytics.datasim.util.errors :as errors] - [com.yetanalytics.datasim.util.io :as dio]) + (:require [clojure.tools.cli :as cli] + [com.yetanalytics.datasim.cli.input :as cli-input] + [com.yetanalytics.datasim.cli.generate :as cli-gen] + [com.yetanalytics.datasim.cli.util :as u]) (:gen-class)) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; CLI Input -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(defn- conj-input - "Conj the input (either a Profile or a personae) to return a - vector of inputs, e.g. `-p profile-1 -p profile-2` becomes - `[profile-1 profile-2]`." - [opt-map id v] - (update opt-map id (fnil conj []) v)) - -(defn- conj-param-input - "Add a parameter named by id." - [opt-map id v] - (update-in opt-map [:parameters id] (fnil conj []) v)) - -(def ^:private validate-input-options - [["-p" "--profile URI" "xAPI Profile Location" - :id :profiles - :desc "The location of an xAPI profile, can be used multiple times." - :parse-fn (partial input/from-location :profile :json) - :assoc-fn conj-input] - ["-a" "--actor-personae URI" "Actor Personae Location" - :id :personae-array - :desc "The location of an Actor Personae document indicating the actors in the sim." - :parse-fn (partial input/from-location :personae :json) - :assoc-fn conj-input] - ["-m" "--models URI" "Persona Model Location" - :id :models - :desc "The location of an Persona Model document, to describe alignments and overrides for the personae." - :parse-fn (partial input/from-location :models :json)] - ["-o" "--parameters URI" "Parameters Location" - :id :parameters - :desc "The location of simulation parameters document." - :parse-fn (partial input/from-location :parameters :json) - :default (params/apply-defaults)] - ["-i" "--input URI" "Pre-validated input location" - :id :input - :desc "The location of a JSON file containing a combined simulation input spec." - :parse-fn (partial input/from-location :input :json)] - ["-c" "--combined-input URI" "Validated combined input location" - :id :location - :desc "The location of the validated input to be produced."]]) - -(def ^:private input-options - [["-p" "--profile URI" "xAPI Profile Location" - :id :profiles - :desc "The location of an xAPI profile, can be used multiple times." - :parse-fn (partial input/from-location :profile :json) - :validate [(partial input/validate-throw :profile) - "Failed to validate profile."] - :assoc-fn conj-input] - ["-a" "--actor-personae URI" "Actor Personae Location" - :id :personae-array - :desc "The location of an Actor Personae document indicating the actors in the sim." - :parse-fn (partial input/from-location :personae :json) - :validate [(partial input/validate-throw :personae) - "Failed to validate personae."] - :assoc-fn conj-input] - ["-m" "--models URI" "Persona Model Location" - :id :models - :desc "The location of an Persona Model document, to describe alignments and overrides for the personae." - :parse-fn (partial input/from-location :models :json) - :validate [(partial input/validate-throw :models) - "Failed to validate Models."]] - ["-o" "--parameters URI" "Parameters Location" - :id :parameters - :desc "The location of simulation parameters document." - :parse-fn (partial input/from-location :parameters :json) - :validate [(partial input/validate-throw :parameters) - "Failed to validate Parameters."] - :default (params/apply-defaults)] - ["-i" "--input URI" "Pre-validated input location" - :id :input - :desc "The location of a JSON file containing a combined simulation input spec." - :parse-fn (partial input/from-location :input :json) - :validate [(partial input/validate-throw :input) - "Failed to validate input."]]]) - -(def ^:private generate-options - [[nil "--seed SEED" "Override input seed" - :id :override-seed - :parse-fn parse-long - :validate [int? "Seed is not an integer."] - :desc "An integer seed to override the one in the input spec. Use -1 for random."] - [nil "--actor AGENT_ID" "Select actor(s) by agent ID" - :id :select-agents - :multi true - :update-fn (fnil conj #{}) - :desc "Pass an agent id in the format 'mbox::malito:[email]' to select actor(s)"] - [nil "--gen-profile IRI" "Only generate based on primary patterns in the given profile. May be given multiple times to include multiple profiles." - :id :gen-profiles - :assoc-fn conj-param-input] - [nil "--gen-pattern IRI" "Only generate based on the given primary pattern. May be given multiple times to include multiple patterns." - :id :gen-patterns - :assoc-fn conj-param-input]]) - -(def ^:private post-options - [["-E" "--endpoint URI" "LRS Endpoint for POST" - :id :endpoint - :desc "The xAPI endpoint of an LRS to POST to, ex: https://lrs.example.org/xapi" - :missing "[-E|--endpoint] argument is required for POST."] - ["-U" "--username URI" "LRS Basic auth username" - :id :username - :desc "The Basic Auth username for the LRS."] - ["-P" "--password URI" "LRS Basic auth password" - :id :password - :desc "The Basic Auth password for the LRS."] - ["-B" "--batch-size SIZE" "LRS POST batch size" - :id :batch-size - :default 25 - :parse-fn parse-long - :validate [int? "Batch size is not an integer."] - :desc "The batch size, i.e. how many statements to send at a time, for POSTing."] - ["-C" "--concurrency CONC" "LRS POST concurrency" - :id :concurrency - :default 4 - :parse-fn parse-long - :validate [int? "Concurrency is not an integer."] - :desc "The max concurrency of the LRS POST pipeline."] - ["-L" "--post-limit LIMIT" "LRS POST total statement limit" - :id :post-limit - :default 999 - :parse-fn parse-long - :validate [int? "POST statement limit is not an integer."] - :desc "The total number of statements that will be sent to the LRS before termination. Overrides sim params. Set to -1 for no limit."] - ["-A" "--[no-]async" "Async operation. Use --no-async if statements must be sent to server in timestamp order." - :id :async - :default true]]) - -(defn cli-options - "Generate CLI options, skipping validation if `validate?` is false" - [validate?] - [["-p" "--profile URI" "xAPI Profile Location" - :id :profiles - :desc "The location of an xAPI profile, can be used multiple times." - :parse-fn (partial input/from-location :profile :json) - :validate (if validate? - [(partial input/validate-throw :profile) - "Failed to validate profile."] - []) - :assoc-fn conj-input] - ["-a" "--actor-personae URI" "Actor Personae Location" - :id :personae-array - :desc "The location of an Actor Personae document indicating the actors in the sim." - :parse-fn (partial input/from-location :personae :json) - :validate (if validate? - [(partial input/validate-throw :personae) - "Failed to validate personae."] - []) - :assoc-fn conj-input] - ["-m" "--models URI" "Persona Model Location" - :id :models - :desc "The location of an Persona Model document, to describe alignments and overrides for the personae." - :parse-fn (partial input/from-location :models :json) - :validate (if validate? - [(partial input/validate-throw :models) - "Failed to validate Models."] - [])] - ["-o" "--parameters URI" "Sim Parameters Location" - :id :parameters - :desc "The location of a Sim Parameters Document." - :parse-fn (partial input/from-location :parameters :json) - :validate (if validate? - [(partial input/validate-throw :parameters) - "Failed to validate Parameters."] - []) - :default (params/apply-defaults)] - - ["-i" "--input URI" "Combined Simulation input" - :id :input - :desc "The location of a JSON file containing a combined simulation input spec." - :parse-fn (partial input/from-location :input :json) - :validate (if validate? - [(partial input/validate-throw :input) - "Failed to validate input."] - [])] - [nil "--seed SEED" "Override input seed" - :id :override-seed - :parse-fn parse-long - :validate [int? "Seed is not an integer."] - :desc "An integer seed to override the one in the input spec. Use -1 for random."] - [nil "--actor AGENT_ID" "Select actor(s) by agent ID" - :id :select-agents - :multi true - :update-fn (fnil conj #{}) - :desc "Pass an agent id in the format mbox::malto:bob@example.org to select actor(s)"] - ;; POST options - ["-E" "--endpoint URI" "LRS Endpoint for POST" - :id :endpoint - :desc "The xAPI endpoint of an LRS to POST to, ex: https://lrs.example.org/xapi"] - ["-U" "--username URI" "LRS Basic auth username" - :id :username - :desc "The basic auth username for the LRS you wish to post to"] - ["-P" "--password URI" "LRS Basic auth password" - :id :password - :desc "The basic auth password for the LRS you wish to post to"] - ["-B" "--batch-size SIZE" "LRS POST batch size" - :id :batch-size - :default 25 - :parse-fn parse-long - :validate [int? "Batch size is not an integer."] - :desc "The batch size for POSTing to an LRS"] - ["-C" "--concurrency CONC" "LRS POST concurrency" - :id :concurrency - :default 4 - :parse-fn parse-long - :validate [int? "Concurrency is not an integer."] - :desc "The max concurrency of the LRS POST pipeline"] - ["-L" "--post-limit LIMIT" "LRS POST total statement limit" - :id :post-limit - :default 999 - :parse-fn parse-long - :validate [int? "POST statement limit is not an integer."] - :desc "The total number of statements that will be sent to the LRS before termination. Overrides sim params. Set to -1 for no limit."] - ["-A" "--[no-]async" "Async operation. Use --no-async if statements must be sent to server in timestamp order." - :id :async - :default true] - [nil "--gen-profile IRI" "Only generate based on primary patterns in the given profile. May be given multiple times to include multiple profiles." - :id :gen-profiles - :assoc-fn conj-param-input] - [nil "--gen-pattern IRI" "Only generate based on the given primary pattern. May be given multiple times to include multiple patterns." - :id :gen-patterns - :assoc-fn conj-param-input] - ;; Help - ["-h" "--help"]]) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; CLI Run -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(defn bail! - "Print error messages to std error and exit." - [errors & {:keys [status] - :or {status 1}}] - (dio/println-err-coll errors) - (System/exit status)) - -(defn- sim-input [options] - (let [sim-options (select-keys options [:input - :profiles - :personae-array - :parameters - :models]) - {:keys [override-seed]} options] - (cond-> (or (:input sim-options) - (dissoc sim-options :input)) - override-seed - (assoc-in [:parameters :seed] - (if (= -1 override-seed) - (random/rand-unbound-int (random/rng)) - override-seed))))) - -;; When this is called, we should have valid individual inputs. However, there -;; may be cross-validation that needs to happen, so we compose the -;; comprehensive spec from the options and check that. -(defn- assert-valid-input [input] - (when-let [errors (not-empty (input/validate :input input))] - (bail! (errors/map-coll->strs errors)))) - -;; POST to LRS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(defn- post-async! - [input post-options post-limit select-agents concurrency] - (let [gen-input (cond-> input - ;; when async, we just use the post - ;; limit as the max - (not= post-limit -1) - (assoc-in [:parameters :max] post-limit)) - sim-chan (ds/generate-seq-async - gen-input - :select-agents select-agents) - result-chan (client/post-statements-async - post-options - sim-chan - :concurrency concurrency)] - (loop [] - (when-let [[tag ret] (a/> (ds/generate-seq - input - :select-agents select-agents) - (not= post-limit -1) - (take post-limit)) - {:keys [fail]} (client/post-statements post-options statements)] - (when (not-empty fail) - (bail! (for [{:keys [status error]} fail] - (client/post-error-message status error)))))) - -(defn- post-sim! - [input options] - (let [{:keys [endpoint - username - password - batch-size - concurrency - post-limit - select-agents - async]} - options] - ;; Endpoint is required when posting - (when-not endpoint - (bail! ["-E / --endpoint REQUIRED for post."])) - ;; Endpoint present - OK - (let [post-options {:endpoint endpoint - :batch-size batch-size - :username username - :password password}] - (if async - (post-async! input post-options post-limit select-agents concurrency) - (post-sync! input post-options post-limit select-agents))))) - -;; Print sim to stdout ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(defn- print-sim! - "Generate statement seqs and writes them to stdout." - [input {:keys [select-agents]}] - (doseq [statement (ds/generate-seq input :select-agents select-agents)] - (dio/write-json-stdout statement :key-fn? false))) - -;; Write Input mode ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(defn- write-input! - ([input] - (input/to-out input :json)) - ([input location] - (input/to-file input :json location) - (println (format "Input specification written to %s" location)))) - -;; Main function ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - (def top-level-options [["-h" "--help" "Display the top-level help guide."]]) @@ -364,52 +18,6 @@ "\n" "Run 'datasim --help' for more info on each subcommand.")) -(defn- exec-subcommand [cli-options exec-fn args] - (let [{:keys [options summary errors]} - (cli/parse-opts args cli-options) - {:keys [help]} - options - errors* (not-empty errors)] - (cond - help (println summary) - errors* (bail! errors*) - :else (exec-fn options)))) - -(defn- validate-input [args] - (exec-subcommand - (conj validate-input-options - ["-h" "--help"]) - (fn [{:keys [location] :as options}] - (let [input (sim-input options)] - (assert-valid-input input) - (if location - (write-input! input location) - (write-input! input)))) - args)) - -(defn- generate [args] - (exec-subcommand - (concat input-options - generate-options - [["-h" "--help"]]) - (fn [options] - (let [input (sim-input options)] - (assert-valid-input input) - (print-sim! input options))) - args)) - -(defn- generate-post [args] - (exec-subcommand - (concat input-options - generate-options - post-options - [["-h" "--help"]]) - (fn [options] - (let [input (sim-input options)] - (assert-valid-input input) - (post-sim! input options))) - args)) - (defn -main [& args] (let [{:keys [options arguments summary errors]} (cli/parse-opts args top-level-options @@ -424,41 +32,7 @@ (print "No subcommand entered.\n\n" summary) :else (case subcommand - "validate-input" (validate-input rest-args) - "generate" (generate rest-args) - "generate-post" (generate-post rest-args) - (bail! errors)))) - #_(let [;; If the verb is "validate-input", we - ;; skip tools.cli validation and do a - ;; more in-depth one. - cli-opts - (cli-options (not= "validate-input" (last args))) - {:keys [options arguments summary errors]} - (cli/parse-opts args cli-opts) - [?command & rest-args] - arguments] - (cond - ;; Invalid CLI input - (seq errors) - (bail! errors) - ;; Help - (or (empty? args) (:help options)) - (println summary) - :else - (let [input (sim-input options)] - (assert-valid-input input) - (case ?command - ;; Where the CLI will actually perform generation - "generate" - (if (= "post" (first rest-args)) - (post-sim! input options) - (print-sim! input options)) - ;; If they just want to validate and we're this far, we're done. - ;; Just return the input spec as JSON. - "validate-input" - (if-some [location (first rest-args)] - (write-input! input location) - (write-input! input)) - ;; No command - (do (println "No command entered.") - (println summary))))))) + "validate-input" (cli-input/validate-input rest-args) + "generate" (cli-gen/generate rest-args) + "generate-post" (cli-gen/generate-post rest-args) + (u/bail! errors))))) From 4ae3a27dcc51524f952f7c517688d446c6a285ec Mon Sep 17 00:00:00 2001 From: kelvinqian00 Date: Thu, 5 Oct 2023 11:43:20 -0400 Subject: [PATCH 05/11] Change -c to -v, --validated-input --- Makefile | 4 ++-- src/cli/com/yetanalytics/datasim/cli/input.clj | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index 2ca3af21..d3925223 100644 --- a/Makefile +++ b/Makefile @@ -44,10 +44,10 @@ test-unit-onyx: clojure -Adev:cli:onyx:run-onyx-tests test-cli: - clojure -A:cli:run validate-input -p dev-resources/profiles/cmi5/fixed.json -a dev-resources/personae/simple.json -m dev-resources/models/simple.json -o dev-resources/parameters/simple.json -c dev-resources/input/simple.json + clojure -A:cli:run validate-input -p dev-resources/profiles/cmi5/fixed.json -a dev-resources/personae/simple.json -m dev-resources/models/simple.json -o dev-resources/parameters/simple.json -v dev-resources/input/simple.json test-cli-comprehensive: - clojure -A:cli:run validate-input -i dev-resources/input/simple.json -c dev-resources/input/simple.json + clojure -A:cli:run validate-input -i dev-resources/input/simple.json -v dev-resources/input/simple.json test-cli-output: clojure -A:cli:run generate -i dev-resources/input/simple.json diff --git a/src/cli/com/yetanalytics/datasim/cli/input.clj b/src/cli/com/yetanalytics/datasim/cli/input.clj index ba7bf6ce..36628713 100644 --- a/src/cli/com/yetanalytics/datasim/cli/input.clj +++ b/src/cli/com/yetanalytics/datasim/cli/input.clj @@ -25,8 +25,8 @@ (def input-desc "The location of a JSON file containing a combined simulation input spec.") -(def combined-input-desc - "The location of the validated input to be produced.") +(def validated-input-desc + "The location of the validated input to be produced. If not provided, the validated input will be printed to stdout instead.") ;; NOTE: For the `validate-input` subcommand, we skip tools.cli validation and ;; do more in-depth validation involving combined inputs. @@ -54,9 +54,9 @@ :id :input :desc input-desc :parse-fn (partial input/from-location :input :json)] - ["-c" "--combined-input URI" "Validated combined input location" - :id :location - :desc combined-input-desc]]) + ["-v" "--validated-input URI" "Validated combined input location" + :id :validated-input + :desc validated-input-desc]]) (def input-options [["-p" "--profile URI" "xAPI Profile Location" @@ -145,10 +145,10 @@ (u/exec-subcommand (conj validate-input-options ["-h" "--help"]) - (fn [{:keys [location] :as options}] + (fn [{:keys [validated-input] :as options}] (let [input (sim-input options)] (assert-valid-input input) - (if location - (write-input! input location) + (if validated-input + (write-input! input validated-input) (write-input! input)))) args)) From 9de12db6dcc536f78a54b02ca42999389fc731c6 Mon Sep 17 00:00:00 2001 From: kelvinqian00 Date: Thu, 5 Oct 2023 11:51:13 -0400 Subject: [PATCH 06/11] Remove parameters defaults from display --- README.md | 2 +- src/cli/com/yetanalytics/datasim/cli/input.clj | 17 +++++++---------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index f1ac524f..e2e14a3b 100644 --- a/README.md +++ b/README.md @@ -206,7 +206,7 @@ The `validate-input` subcommand is used to validate and combine input files. The | `-p, --profile URI` | The location of an xAPI profile, can be used multiple times. | `-a, --actor-personae URI` | The location of an Actor Personae document indicating the actors in the sim. | `-m, --models URI` | The location of an Persona Model document, to describe alignments and overrides for the personae. -| `-o, -parameters URI` | The location of simulation parameters document. (The "o" stands for "options.") +| `-o, -parameters URI` | The location of simulation parameters document. Uses the current time and timezone as defaults if they are not present. (The "o" stands for "options.") | `-i, --input URI` | The location of a JSON file containing a combined simulation input spec. | `-c, --combined-input URI` | The location of the validated input to be produced. diff --git a/src/cli/com/yetanalytics/datasim/cli/input.clj b/src/cli/com/yetanalytics/datasim/cli/input.clj index 36628713..2e8d28a8 100644 --- a/src/cli/com/yetanalytics/datasim/cli/input.clj +++ b/src/cli/com/yetanalytics/datasim/cli/input.clj @@ -1,10 +1,9 @@ (ns com.yetanalytics.datasim.cli.input "CLI options and functions for sim inputs (including input validation)." - (:require [com.yetanalytics.datasim.cli.util :as u] - [com.yetanalytics.datasim.input :as input] - [com.yetanalytics.datasim.input.parameters :as params] - [com.yetanalytics.datasim.math.random :as random] - [com.yetanalytics.datasim.util.errors :as errors])) + (:require [com.yetanalytics.datasim.cli.util :as u] + [com.yetanalytics.datasim.input :as input] + [com.yetanalytics.datasim.math.random :as random] + [com.yetanalytics.datasim.util.errors :as errors])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; CLI Input Options @@ -20,7 +19,7 @@ "The location of an Persona Model document, to describe alignments and overrides for the personae.") (def parameters-desc - "The location of simulation parameters document.") + "The location of simulation parameters document. Uses the current time and timezone as defaults if they are not present.") (def input-desc "The location of a JSON file containing a combined simulation input spec.") @@ -48,8 +47,7 @@ ["-o" "--parameters URI" "Parameters Location" :id :parameters :desc parameters-desc - :parse-fn (partial input/from-location :parameters :json) - :default (params/apply-defaults)] + :parse-fn (partial input/from-location :parameters :json)] ["-i" "--input URI" "Pre-validated input location" :id :input :desc input-desc @@ -84,8 +82,7 @@ :desc parameters-desc :parse-fn (partial input/from-location :parameters :json) :validate [(partial input/validate-throw :parameters) - "Failed to validate Parameters."] - :default (params/apply-defaults)] + "Failed to validate Parameters."]] ["-i" "--input URI" "Pre-validated input location" :id :input :desc input-desc From 451108302952473457bd3af3e9ad91e11415a021 Mon Sep 17 00:00:00 2001 From: kelvinqian00 Date: Thu, 5 Oct 2023 11:53:39 -0400 Subject: [PATCH 07/11] Add defaults to POST args + align tables --- README.md | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index e2e14a3b..4b08a153 100644 --- a/README.md +++ b/README.md @@ -201,33 +201,33 @@ With no commands or `--help` it will give you the list of subcommands: The `validate-input` subcommand is used to validate and combine input files. These are its arguments: -| Argument | Description -| --- | --- -| `-p, --profile URI` | The location of an xAPI profile, can be used multiple times. +| Argument | Description +| --- | --- +| `-p, --profile URI` | The location of an xAPI profile, can be used multiple times. | `-a, --actor-personae URI` | The location of an Actor Personae document indicating the actors in the sim. -| `-m, --models URI` | The location of an Persona Model document, to describe alignments and overrides for the personae. -| `-o, -parameters URI` | The location of simulation parameters document. Uses the current time and timezone as defaults if they are not present. (The "o" stands for "options.") -| `-i, --input URI` | The location of a JSON file containing a combined simulation input spec. +| `-m, --models URI` | The location of an Persona Model document, to describe alignments and overrides for the personae. +| `-o, -parameters URI` | The location of simulation parameters document. Uses the current time and timezone as defaults if they are not present. (The "o" stands for "options.") +| `-i, --input URI` | The location of a JSON file containing a combined simulation input spec. | `-c, --combined-input URI` | The location of the validated input to be produced. The `generate` subcommand is used to generate statements from an input and print them to standard output. The inputs can be a combined `--input` location or a combination of `-p`, `-a`, `-m`, and `-o` inputs. The additional arguments are as follows: -| Argument | Description -| --- | --- -| `--seed SEED` | An integer seed to override the one in the input spec. Use -1 for a random seed. -| `--actor AGENT_ID` | Pass an agent id in the format 'mbox::mailto:[email]' to select actor(s) +| Argument | Description +| --- | --- +| `--seed SEED` | An integer seed to override the one in the input spec. Use -1 for a random seed. +| `--actor AGENT_ID` | Pass an agent id in the format 'mbox::mailto:[email]' to select actor(s) | `--gen-profile IRI` | Only generate based on primary patterns in the given profile. May be given multiple times to include multiple profiles. | `--gen-pattern IRI` | Only generate based on the given primary pattern. May be given multiple times to include multiple patterns. The `generate-post` subcommand is used to generate statements from an input and POST them to an LRS. In addition to the `generate` arguments, this subcommands has these additional arguments: -| Argument | Description -| --- | --- -| `-E, --endpoint URI` | The xAPI endpoint of an LRS to POST to, ex: `https://lrs.example.org/xapi` -| `-U, --username URI` | The Basic Auth username for the LRS. -| `-P, --password URI` | The Basic Auth password for the LRS. -| `-B, --batch-size SIZE` | The batch size, i.e. how many statements to send at a time, for POSTing. -| `-C, --concurrency CONC` | The max concurrency of the LRS POST pipeline. -| `-L, --post-limit LIMIT` | The total number of statements that will be sent to the LRS before termination. Overrides sim params. Set to -1 for no limit. -| `-A, --[no-]async` | Async operation. Use `--no-async`` if statements must be sent to server in timestamp order. +| Argument | Description | Default +| --- | --- | --- +| `-E, --endpoint URI` | The xAPI endpoint of an LRS to POST to, ex: `https://lrs.example.org/xapi` | N/A +| `-U, --username URI` | The Basic Auth username for the LRS. | N/A +| `-P, --password URI` | The Basic Auth password for the LRS. | N/A +| `-B, --batch-size SIZE` | The batch size, i.e. how many statements to send at a time, for POSTing. | `25` +| `-C, --concurrency CONC` | The max concurrency of the LRS POST pipeline. | `4` +| `-L, --post-limit LIMIT` | The total number of statements that will be sent to the LRS before termination. Overrides sim params. Set to -1 for no limit. | `999` +| `-A, --[no-]async` | Async operation. Use `--no-async` if statements must be sent to server in timestamp order. | `true` The following is an example of a simple run. We first create a combined input file using `validate-input`: ``` From 6084b3851b253b361494cd2b962a8b321e78bcef Mon Sep 17 00:00:00 2001 From: kelvinqian00 Date: Thu, 5 Oct 2023 14:43:42 -0400 Subject: [PATCH 08/11] Change -c to -v in README --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 4b08a153..eb0408dc 100644 --- a/README.md +++ b/README.md @@ -201,14 +201,14 @@ With no commands or `--help` it will give you the list of subcommands: The `validate-input` subcommand is used to validate and combine input files. These are its arguments: -| Argument | Description -| --- | --- -| `-p, --profile URI` | The location of an xAPI profile, can be used multiple times. -| `-a, --actor-personae URI` | The location of an Actor Personae document indicating the actors in the sim. -| `-m, --models URI` | The location of an Persona Model document, to describe alignments and overrides for the personae. -| `-o, -parameters URI` | The location of simulation parameters document. Uses the current time and timezone as defaults if they are not present. (The "o" stands for "options.") -| `-i, --input URI` | The location of a JSON file containing a combined simulation input spec. -| `-c, --combined-input URI` | The location of the validated input to be produced. +| Argument | Description +| --- | --- +| `-p, --profile URI` | The location of an xAPI profile, can be used multiple times. +| `-a, --actor-personae URI` | The location of an Actor Personae document indicating the actors in the sim. +| `-m, --models URI` | The location of an Persona Model document, to describe alignments and overrides for the personae. +| `-o, -parameters URI` | The location of simulation parameters document. Uses the current time and timezone as defaults if they are not present. (The "o" stands for "options.") +| `-i, --input URI` | The location of a JSON file containing a combined simulation input spec. +| `-v, --validated-input URI` | The location of the validated input to be produced. The `generate` subcommand is used to generate statements from an input and print them to standard output. The inputs can be a combined `--input` location or a combination of `-p`, `-a`, `-m`, and `-o` inputs. The additional arguments are as follows: | Argument | Description @@ -236,7 +236,7 @@ bin/run.sh validate-input \ -a dev-resources/personae/simple.json \ -m dev-resources/models/simple.json \ -o dev-resources/parameters/simple.json \ - -c dev-resources/input/simple.json + -v dev-resources/input/simple.json ``` Once we have that sim specification, we can run the simulation using the `generate`: From 0d86c0558b1a7b2748040871258e5595ec86c9a2 Mon Sep 17 00:00:00 2001 From: kelvinqian00 Date: Thu, 5 Oct 2023 15:02:39 -0400 Subject: [PATCH 09/11] Rename main ns in cli to cli.clj --- deps.edn | 3 ++- src/cli/com/yetanalytics/datasim/{main.clj => cli.clj} | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) rename src/cli/com/yetanalytics/datasim/{main.clj => cli.clj} (97%) diff --git a/deps.edn b/deps.edn index 6993b00a..404c5371 100644 --- a/deps.edn +++ b/deps.edn @@ -18,7 +18,8 @@ :aliases {:cli {:extra-paths ["src/cli"] :extra-deps {org.clojure/tools.cli {:mvn/version "1.0.219"}}} - :run {:main-opts ["-m" "com.yetanalytics.datasim.main"]} + ;; TODO: More CLI-specific name for :run alias + :run {:main-opts ["-m" "com.yetanalytics.datasim.cli"]} :dev {:extra-paths ["dev-resources" "src/dev"] :extra-deps {incanter/incanter-core {:mvn/version "1.9.3"} incanter/incanter-charts {:mvn/version "1.9.3"} diff --git a/src/cli/com/yetanalytics/datasim/main.clj b/src/cli/com/yetanalytics/datasim/cli.clj similarity index 97% rename from src/cli/com/yetanalytics/datasim/main.clj rename to src/cli/com/yetanalytics/datasim/cli.clj index f3cbf972..714141b0 100644 --- a/src/cli/com/yetanalytics/datasim/main.clj +++ b/src/cli/com/yetanalytics/datasim/cli.clj @@ -1,4 +1,4 @@ -(ns com.yetanalytics.datasim.main +(ns com.yetanalytics.datasim.cli (:require [clojure.tools.cli :as cli] [com.yetanalytics.datasim.cli.input :as cli-input] [com.yetanalytics.datasim.cli.generate :as cli-gen] From 12aec9a5aac64801aab55f14c16c17875c6f8189 Mon Sep 17 00:00:00 2001 From: kelvinqian00 Date: Thu, 5 Oct 2023 15:19:27 -0400 Subject: [PATCH 10/11] Correct main class in Makefile --- Makefile | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index d3925223..ab4d4c40 100644 --- a/Makefile +++ b/Makefile @@ -2,8 +2,7 @@ GROUP_ID ?= com.yetanalytics ARTIFACT_ID ?= datasim -VERSION ?= 0.3.0 -MAIN_NS ?= com.yetanalytics.datasim.main +VERSION ?= 0.4.0 clean: rm -rf target @@ -11,7 +10,7 @@ clean: target/bundle/datasim_cli.jar: mkdir -p target/bundle rm -f pom.xml - clojure -X:depstar uberjar :no-pom false :sync-pom true :aliases '[:cli]' :aot true :group-id $(GROUP_ID) :artifact-id $(ARTIFACT_ID)-cli :version '"$(VERSION)"' :jar target/bundle/datasim_cli.jar :main-class com.yetanalytics.datasim.main + clojure -X:depstar uberjar :no-pom false :sync-pom true :aliases '[:cli]' :aot true :group-id $(GROUP_ID) :artifact-id $(ARTIFACT_ID)-cli :version '"$(VERSION)"' :jar target/bundle/datasim_cli.jar :main-class com.yetanalytics.datasim.cli rm -f pom.xml target/bundle/datasim_server.jar: # no AOT for this one @@ -35,7 +34,7 @@ target/bundle: target/bundle/bin target/bundle/datasim_cli.jar target/bundle/dat bundle: target/bundle - +# Tests test-unit: clojure -Adev:cli:run-tests @@ -53,7 +52,7 @@ test-cli-output: clojure -A:cli:run generate -i dev-resources/input/simple.json test-bundle-output: bundle - cd target/bundle; bin/run.sh generate -i ../../dev-resources/input/simple.json + cd target/bundle; bin/run.sh generate -i ../../dev-resources/input/simple.json validate-template: AWS_PAGER="" aws cloudformation validate-template --template-body file://template/0_vpc.yml From b7632f52b0336bafedb859873ac754395e96c76b Mon Sep 17 00:00:00 2001 From: kelvinqian00 Date: Thu, 9 Nov 2023 12:07:44 -0500 Subject: [PATCH 11/11] Fix math.random to util.random --- src/cli/com/yetanalytics/datasim/cli/input.clj | 2 +- src/test/com/yetanalytics/datasim/xapi/statement_test.clj | 2 +- src/test/com/yetanalytics/datasim_test.clj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cli/com/yetanalytics/datasim/cli/input.clj b/src/cli/com/yetanalytics/datasim/cli/input.clj index 2e8d28a8..2ab64dcf 100644 --- a/src/cli/com/yetanalytics/datasim/cli/input.clj +++ b/src/cli/com/yetanalytics/datasim/cli/input.clj @@ -2,7 +2,7 @@ "CLI options and functions for sim inputs (including input validation)." (:require [com.yetanalytics.datasim.cli.util :as u] [com.yetanalytics.datasim.input :as input] - [com.yetanalytics.datasim.math.random :as random] + [com.yetanalytics.datasim.util.random :as random] [com.yetanalytics.datasim.util.errors :as errors])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/src/test/com/yetanalytics/datasim/xapi/statement_test.clj b/src/test/com/yetanalytics/datasim/xapi/statement_test.clj index fd04f977..d8ce9dc5 100644 --- a/src/test/com/yetanalytics/datasim/xapi/statement_test.clj +++ b/src/test/com/yetanalytics/datasim/xapi/statement_test.clj @@ -1209,7 +1209,7 @@ statements (gen-weighted-statements {:verbs weights}) verb-ids (map #(get-in % ["verb" "id"]) statements) verb-freqs (frequencies verb-ids)] - ;; See `datasim.math.random-test` for details on how the expected means + ;; See `datasim.util.random-test` for details on how the expected means ;; (800 and 200, respectively) are computed. (is (= 793 (get verb-freqs "https://w3id.org/xapi/adl/verbs/abandoned"))) (is (= 207 (get verb-freqs "https://w3id.org/xapi/adl/verbs/satisfied"))) diff --git a/src/test/com/yetanalytics/datasim_test.clj b/src/test/com/yetanalytics/datasim_test.clj index db95fbc0..82b602eb 100644 --- a/src/test/com/yetanalytics/datasim_test.clj +++ b/src/test/com/yetanalytics/datasim_test.clj @@ -319,7 +319,7 @@ objects (map get-object result) obj-count (count objects) obj-freq (frequencies objects) - ;; See `datasim.math.random` for math details + ;; See `datasim.util.random` for math details mean-1* (- 1 (/ 0.3 (* 2 0.7))) mean-2* (- 1 mean-1*) mean-1 (* obj-count mean-1*)