Skip to content

Commit

Permalink
Merge pull request #40 from yetanalytics/webserver
Browse files Browse the repository at this point in the history
Webserver
  • Loading branch information
kelvinqian00 authored May 1, 2023
2 parents ce37578 + 8073bf9 commit 7f8d053
Show file tree
Hide file tree
Showing 20 changed files with 1,026 additions and 152 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
- Add CLI for Persephone as two commands that can be made using `make bundle`; command options can be viewed using `--help`.
- `validate`: Performs Statement Template validation for a single Statement
- `match`: Performs Pattern matching for a Statement batch
- Add webserver that can be started in either `validate` or `match` mode, then perform validation/matching by providing Statements at the `POST /statements` endpoint.
- Move Clojure and ClojureScript dependencies out of main deps into alias (extra) deps
- Added printing for pattern match errors (not just failures)
- Change Statement validation error message header and assert message from `Invalid Statement` to `Statement Validation Failure`
Expand Down
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,14 @@ ci: clean test-clj test-cljs
target/bundle/cli.jar:
clojure -T:build uber :jar cli

target/bundle/server.jar:
clojure -T:build uber :jar server

target/bundle/bin:
mkdir -p target/bundle/bin
cp bin/*.sh target/bundle/bin
chmod +x target/bundle/bin/*.sh

target/bundle: target/bundle/cli.jar target/bundle/bin
target/bundle: target/bundle/cli.jar target/bundle/server.jar target/bundle/bin

bundle: target/bundle
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ com.yetanalytics/project-persephone {:mvn/version "0.9.0"}

- [Library](doc/library.md): How to use the library/API functions
- [CLI](doc/cli.md): How to run the `validate` and `match` commands
- [Webserver](doc/server.md): How to start up and run a webserver

## How It Works

Expand Down
3 changes: 3 additions & 0 deletions bin/server.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/sh

java -server -jar server.jar $@
41 changes: 38 additions & 3 deletions deps.edn
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,37 @@
{:cli
{:extra-paths ["src/cli"]
:extra-deps {org.clojure/clojure {:mvn/version "1.10.2"}
org.clojure/tools.cli {:mvn/version "1.0.206"}
cheshire/cheshire {:mvn/version "5.11.0"}}}
org.clojure/tools.cli {:mvn/version "1.0.206"}}}
:server
{:extra-paths ["src/server"]
:extra-deps {org.clojure/clojure {:mvn/version "1.10.2"}
org.clojure/tools.cli {:mvn/version "1.0.206"}
org.slf4j/slf4j-simple {:mvn/version "1.7.28"}
;; pedestal.service
io.pedestal/pedestal.service
{:mvn/version "0.5.10"
:exclusions [org.msgpack/msgpack
cheshire/cheshire
ring/ring-core]}
cheshire/cheshire {:mvn/version "5.11.0"}
ring/ring-core {:mvn/version "1.10.0"}
;; pedestal.jetty
io.pedestal/pedestal.jetty
{:mvn/version "0.5.10"
:exclusions [org.eclipse.jetty/jetty-server
org.eclipse.jetty/jetty-servlet
org.eclipse.jetty.alpn/alpn-api
org.eclipse.jetty/jetty-alpn-server
org.eclipse.jetty.http2/http2-server
org.eclipse.jetty.websocket/websocket-api
org.eclipse.jetty.websocket/websocket-servlet
org.eclipse.jetty.websocket/websocket-server]}
org.eclipse.jetty/jetty-server {:mvn/version "9.4.51.v20230217"}
org.eclipse.jetty/jetty-servlet {:mvn/version "9.4.51.v20230217"}
org.eclipse.jetty.alpn/alpn-api {:mvn/version "1.1.3.v20160715"}
org.eclipse.jetty/jetty-alpn-server {:mvn/version "9.4.51.v20230217"}
org.eclipse.jetty/jetty-alpn-java-server {:mvn/version "9.4.51.v20230217"}
org.eclipse.jetty.http2/http2-server {:mvn/version "9.4.51.v20230217"}}}
:dev
{:extra-paths ["src/dev"]
:extra-deps {org.clojure/clojure {:mvn/version "1.10.2"}
Expand All @@ -33,10 +62,16 @@
criterium/criterium {:mvn/version "0.4.6"} ; clj only
com.taoensso/tufte {:mvn/version "2.2.0"}}}
:test
{:extra-paths ["src/cli" "src/test" "src/gen" "test-resources"]
{:extra-paths ["src/cli" "src/server" "src/test" "test-resources"]
:extra-deps {org.clojure/clojure {:mvn/version "1.10.2"}
org.clojure/clojurescript {:mvn/version "1.10.764"
:exclusions [org.clojure/data.json]}
;; :server deps
io.pedestal/pedestal.service {:mvn/version "0.5.10"}
io.pedestal/pedestal.jetty {:mvn/version "0.5.10"}
;; Superseeded by babashka/http-client but we cannot use that
;; due to cljs shadowing the default `random-uuid` fn
babashka/babashka.curl {:mvn/version "0.1.2"}
org.clojure/test.check {:mvn/version "1.1.0"}
orchestra/orchestra {:mvn/version "2021.01.01-1"}
olical/cljs-test-runner {:mvn/version "3.8.0"
Expand Down
194 changes: 194 additions & 0 deletions doc/server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
# Webserver

Persephone features a webserver that can be used to validate or match Statements at the `POST /statements` endpoint.

To use the server, first run `make bundle`, then `cd` into `target/bundle`. You will then be able to run `/bin/server.sh`, which will accept either the `validate` or `match` subcommand to start the server in either Statement Template validation or Pattern matching mode, respectively. The following table shows the top-level arguments to the server init command:

| Command Argument | Default | Description
| :-- | :-- | :--
| `-H, --host HOST` | `localhost` | The hostname of the webserver endpoint
| `-P, --port PORT` | `8080` | The port number of the webserver endpoint; must be between 0 and 65536
| `-h, --help` | N/A | Display the top-level help guide

The `validate` subcommand starts the server in validate mode, where any Statements sent to the `/statements` endpoint will undergo Template matching against the Profiles that the server was given on startup. If a Statement array is sent, only the last statement will be validated. The following table shows the arguments to `validate`:

| Subcommand Argument | Description
| :-- | :--
| `-p, --profile URI` | Profile URI filepath/location; must specify one or more.
| `-i, --template-id IRI` | IDs of Statement Templates to validate against; can specify zero or more. Filters out all Templates that are not included.
| `-a, --all-valid` | If set, any Statement is not considered valid unless it is valid against ALL Templates. Otherwise, a Statement only needs to be valid against at least one Template.
| `-c, --short-circuit` | If set, then print on only the first Template any Statement fails validation against.Otherwise, print for all Templates a Statement fails against.
| `-h, --help` | Display the 'validate' subcommand help menu.

The `match` subcommand starts the server in match mode, where any Statements sent to the `/statements` endpoint will undergo Pattern matching against the Profiles that the server was given on startup. If a single Statement is sent, it is coerced into a Statement batch. The following table shows the arguments to `match`:

| Subcommand Argument | Description
| :-- | :--
| `-p, --profile URI` | Profile filepath/location; must specify one or more.
| `-i, --pattern-id IRI` | IDs of primary Patterns to match against; can specify zero or more. Filters out all Patterns that are not included.
| `-h, --help` | Display the 'match' subcommand help menu.

In addition to the `POST /statements` endpoint, there is a `GET /health` endpoint that is used to perform a server health check:
```
% curl -i 0.0.0.0:8080/health
```
which will return an response with status `200 OK`:
```http
HTTP/1.1 200 OK
Date: Mon, 01 May 2023 17:25:32 GMT
Strict-Transport-Security: max-age=31536000; includeSubdomains
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
X-Download-Options: noopen
X-Permitted-Cross-Domain-Policies: none
Content-Security-Policy: object-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;
Content-Type: text/plain
Transfer-Encoding: chunked
```
and a body `"OK"`.

There is no `PUT` or `GET` versions of the `/statements` endpoint, unlike what is required in a learning record store.

# Examples for validate mode

For the first few examples, let us start a webserver in validate mode with a single Profile. Assume that we have already copied the contents of `test-profile` into the `target/bundle` directory. Running this command
```
% ./bin/server.sh validate --profile sample_profiles/calibration.jsonld
```
will start up a server in validate mode on `localhost:8080` with a single Profile set to validate against.

To validate a single Statement against Templates in that Profile:
```bash
% curl -i localhost:8080/statements \
-H "Content-Type: application/json" \
-d @sample_statements/calibration_1.json
```
This will return the following `204 No Content` response, indicating validation success:
```http
HTTP/1.1 204 No Content
Date: Mon, 01 May 2023 17:18:14 GMT
Strict-Transport-Security: max-age=31536000; includeSubdomains
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
X-Download-Options: noopen
X-Permitted-Cross-Domain-Policies: none
Content-Security-Policy: object-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;
```

We can also input a Statement array, e.g. `sample_statements/calibration_coll.json`, but only the last Statement in that array will be validated.

If we try to validate an invalid Statement:
```bash
% curl -i localhost:8080/statements \
-H "Content-Type: application/json" \
-d @sample_statements/adl_1.json
```
then we receive a `400 Bad Request` response:
```http
HTTP/1.1 400 Bad Request
Date: Mon, 01 May 2023 17:21:23 GMT
Strict-Transport-Security: max-age=31536000; includeSubdomains
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
X-Download-Options: noopen
X-Permitted-Cross-Domain-Policies: none
Content-Security-Policy: object-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;
Content-Type: application/edn
Transfer-Encoding: chunked
```

and an EDN response body that looks like the following:
```clojure
{:type :validation-failure
:contents {...}}
```
where `:contents` is the return value of `persephone/validate-statement` with `:fn-type :errors`.

We will also receive a `400 Bad Request` error if we input a completely invalid statement:
```bash
% curl -i localhost:8080/statements \
-H "Content-Type: application/json" \
-d '{"id": "not-a-statement"}'
```
The response body will still contain `:type` and `:contents`, but `:type` will have the value `:invalid-statement` and `:contents` will be a Clojure spec error map.

Similarly, if the request is invalid JSON, `:type` will have the value `:invalid-json`.

Validation also works with two Profiles:
```
% ./bin/server.sh validate \
--profile sample_profiles/calibration.jsonld \
--profile sample_profiles/catch.json
```
as well as the `--template-id`, `--all-valid`, and `--short-circuit` flags. These work very similarly to how they work in the [CLI](cli.md#examples-for-persephone-validate). Note that you should take care not to include duplicate IDs or else you will receive an init error.

# Examples for match mode

In match mode, we will first start a webserver mode with a single Profile. Running this command
```
% ./bin/server.sh match --profile sample_profiles/calibration.jsonld
```
will start up a server in validate mode on `localhost:8080` with a single Profile set to perform Pattern matching against.

To validate a Statement array against Templates in that Profile:
```bash
% curl -i localhost:8080/statements \
-H "Content-Type: application/json" \
-d @sample_statements/calibration_coll.json
```
This will return a `204 No Content` response, indicating match success:
```http
HTTP/1.1 204 No Content
Date: Mon, 01 May 2023 17:23:09 GMT
Strict-Transport-Security: max-age=31536000; includeSubdomains
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
X-Download-Options: noopen
X-Permitted-Cross-Domain-Policies: none
Content-Security-Policy: object-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;
```

Likewise, we can match against a statement, in this case passing in the file `sample_statements/calibration_1.json` instead. Notice how similar the request body is to the equivalent request in validate mode.

If we try to Pattern match a Statement sequence that cannot be matched (e.g. we reverse the order of the two Statements in `calibration_coll.json`), we will receive the following EDN `400 Bad Request` response:
```http
HTTP/1.1 400 Bad Request
Date: Mon, 01 May 2023 17:24:10 GMT
Strict-Transport-Security: max-age=31536000; includeSubdomains
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
X-Download-Options: noopen
X-Permitted-Cross-Domain-Policies: none
Content-Security-Policy: object-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;
Content-Type: application/edn
Transfer-Encoding: chunked
```

And the following response body:
```clojure
{:type :match-failure
:contents {...}}
```
where `:contents` is the return value of `persephone/match-statement-batch`.

If we have a match error, e.g. a missing Profile reference in category context activity IDs or an invalid subregistration, the `400 Bad Request` response body will be of the form
```clojure
{:type :match-error
:contents {:errors {...}}}
```
where the `:errors` value is a map containing the error data.

If we have a Statement syntax error, then `:type` will have the value `:invalid-statements` and `:contents` will be a Clojure spec error map. Similarly, if we have invalid JSON, then `:type` will be `:invalid-json`.

As with validation, Pattern matching works with two or more Profiles:
```
% ./bin/server.sh match \
--profile sample_profiles/calibration.jsonld \
--profile sample_profiles/catch.json
```
though you should be careful not to include any duplicate Profile or Pattern IDs or else you will receive an error.
38 changes: 23 additions & 15 deletions src/build/com/yetanalytics/persephone/build.clj
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
(ns com.yetanalytics.persephone.build
(:require [clojure.tools.build.api :as b]))

(def valid-jar-names #{"cli"})
(def valid-jar-names #{"cli" "server"})

(def source-dirs ["src/main" "src/cli"])
(defn- source-directories [jar-name]
["src/main" (format "src/%s" jar-name)])

(def class-dir "target/classes")
(defn- class-directory [jar-name]
(format "target/classes/%s/" jar-name))

(def basis
(defn- project-basis [jar-name]
(b/create-basis {:project "deps.edn"
:aliases [:cli]}))
:aliases [(keyword jar-name)]}))

(defn- uber-file [jar-name]
(defn- uberjar-file [jar-name]
(format "target/bundle/%s.jar" jar-name))

(defn- main-ns [jar-name]
(case jar-name
"cli" 'com.yetanalytics.persephone.cli))
(defn- main-namespace [jar-name]
(symbol (format "com.yetanalytics.persephone.%s" jar-name)))

(defn- validate-jar-name
"Return `jar` as a string if valid, throw otherwise."
[jar]
(let [jar-name (name jar)]
(if (valid-jar-names jar-name)
Expand All @@ -32,18 +34,24 @@
| Keyword Arg | Description
| --- | ---
| `:cli` | Create `cli.jar` for the command line interface."
| `:cli` | Create `cli.jar` for the command line interface.
| `:server` | Create `server.jar` for the webserver."
[{:keys [jar]}]
(let [jar-name (validate-jar-name jar)]
(let [jar-name (validate-jar-name jar)
src-dirs (source-directories jar-name)
class-dir (class-directory jar-name)
basis (project-basis jar-name)
uber-file (uberjar-file jar-name)
main-ns (main-namespace jar-name)]
(b/copy-dir
{:src-dirs source-dirs
{:src-dirs src-dirs
:target-dir class-dir})
(b/compile-clj
{:basis basis
:src-dirs source-dirs
:src-dirs src-dirs
:class-dir class-dir})
(b/uber
{:basis basis
:class-dir class-dir
:uber-file (uber-file jar-name)
:main (main-ns jar-name)})))
:uber-file uber-file
:main main-ns})))
6 changes: 3 additions & 3 deletions src/cli/com/yetanalytics/persephone/cli.clj
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
(ns com.yetanalytics.persephone.cli
(:require [clojure.tools.cli :as cli]
[com.yetanalytics.persephone.utils.cli :as u]
[com.yetanalytics.persephone.cli.match :as m]
[com.yetanalytics.persephone.cli.validate :as v]
[com.yetanalytics.persephone.cli.util.args :refer [printerr]])
[com.yetanalytics.persephone.cli.validate :as v])
(:gen-class))

(def top-level-options
Expand Down Expand Up @@ -39,5 +39,5 @@
(System/exit 1))
:else
(do
(printerr (format "Unknown subcommand: %s" subcommand))
(u/printerr (format "Unknown subcommand: %s" subcommand))
(System/exit 1)))))
Loading

0 comments on commit 7f8d053

Please sign in to comment.