diff --git a/README.md b/README.md index 72bab7d..ee546d6 100644 --- a/README.md +++ b/README.md @@ -387,6 +387,79 @@ route = root $ sum } ``` +## Example: Using Variant codecs to represent polymorphic CRUD operations + +In the previous example we’ve seen how to compose codecs in CRUD operations, but the route data types for those operations were fixed, closed; and yet, in most cases, resources in an application need to support different CRUD operations or only a subset of those. + +In order to solve this problem, we may use the `Variant` data type from `Data.Variant`. This library exports codecs for polymorphic variants: `variant` and `vcase` (and its operator alias `%=`). The API for these combinators follows the idea of `record` and `prop` seen previously. + +In this example we’ll model the same `Post` codec from the previous example, supporting _create_, _read_ and _update_ operations, as well as a `User` codec that supports only the _read_ and _update_ operations, with the caveat that the update operation for users should not take an argument, as a user should only be able to update their own data, and not that of others. + +So a complete description of the routes for posts is: + +- `/` should represent creation +- `/:id` should represent reading +- `/edit/:id` should represent updating + +And for users: + +- `/:id` should represent reading +- `/edit` should represent updating + +Let’s first create some standard type aliases for common CRUD operations that may be used with `Variant` first: + +```purescript +type Create r = (create :: Unit | r) +type Read a r = (read :: a | r) +type Update a r = (update :: a | r) +``` + +We can use these type aliases to build the parts of the `Route` data type that describe the user and post route schemes. In this example, the `+` type operator from `Type.Row` is used (from the `purescript-typelevel-prelude` package) for extra syntactic sugar: + +```purescript +data Route + = ... + | User (Variant (Read Username + Update Unit + ())) + | Post Username (Variant (Create + Read PostId + Update PostId + ())) +``` + +Next, we can create some helper functions for defining codecs for these common operations using `vcase`: + +```purescript +create :: forall r. Lacks "create" r => RouteDuplex' (Variant r) -> RouteDuplex' (Variant (Create r)) +create = vcase (Proxy :: _ "create") (pure unit) + +read :: forall a r. Lacks "read" r => RouteDuplex' a -> RouteDuplex' (Variant r) -> RouteDuplex' (Variant (Read a r)) +read = vcase (Proxy :: _ "read") + +update :: forall a r. Lacks "update" r => RouteDuplex' a -> RouteDuplex' (Variant r) -> RouteDuplex' (Variant (Update a r)) +update = vcase (Proxy :: _ "update") +``` + +And finally, we can use `variant` and the helper codecs we’ve just defined together with `postId` and `uname` to produce the larger `Route` codec: + +```purescript +route = root $ sum + { ... + , "User": + path "user" + $ variant + # read uname + # update (pure unit) + , "Post": + "user" + / uname + / path "post" + ( variant + # create + # read postId + # update postId + ) + } +``` + +It’s important to note here that the read and update routes for users may collide. To solve this ambiguity, we had to define the Variant parser for users in the correct order: `update` takes priority over `read` as it is applied later. To learn more read ["About ordering" section](#about-ordering) + ## Example: Running our codec with `purescript-routing` We've developed a capable parser and printer for our route data type. To be useful, though, we'll want to use our parser along with a library that handles hash-based or pushState routing for us. The most common choice is the `purescript-routing` library. If you aren't familiar with how the library works, [consider skimming the official guide](https://github.com/slamdata/purescript-routing/blob/v8.0.0/GUIDE.md). @@ -422,3 +495,41 @@ To perform your routing effects, provide your custom callback function: canceller <- matchesWith (parse route) \old new -> do ... your routing effects, called every time the route changes ... ``` + +# About ordering + +### For `variant` and `vcase` ORDERING OF FUNCTIONS is important + +1. `(_x %= segment) ((_y %= segment) variant)` the `_x` will be tried first (it always from left to right) +2. `(_y %= segment) ((_x %= segment) variant)` the `_y` will be tried first (it always from left to right) + +Therefore, strict parsers should start on left. + +Example: + +1. `(_x %= pure unit) ((_y %= segment) variant)` - bad, because `_x` will always succeed. +2. `(_y %= segment) ((_x %= pure unit) variant)` - ok. + +### For `sum` ORDERING OF A CONSTRUCTORS is important + +1. `data ABRoute = A String | B String; RDG.sum { "A": segment, "B" segment }` the `A` constructor will always be tried first (because generated by Generic class) +2. `data ABRoute = B String | A String; RDG.sum { "A": segment, "B" segment }` the `B` constructor will always be tried first + +Therefore, strict parsers should start on leading constructors. + +Example: + +1. `data ABRoute = A Unit | B String; RDG.sum { "A": pure unit, "B": segment }` - bad, because `A` will always succeed. +2. `data ABRoute = B String | A Unit; RDG.sum { "A": pure unit, "B": segment }` - ok. + +### For `vmatch` NAMES OF KEYS is important + +1. `vmatch { x: segment, y: segment }` the `x` will always be tried first +2. `vmatch { y: segment, x: segment }` the `x` will always be tried first too (ordering doesnt matter) + +Therefore, strict parsers should start on leading letters. + +Example: + +1. `vmatch { x: pure unit, y: segment }` - bad, because `x` will always succeed. +2. `vmatch { r2: pure unit, r1: segment }` - ok. diff --git a/spago.dhall b/spago.dhall index 2a09cf8..2728fe7 100644 --- a/spago.dhall +++ b/spago.dhall @@ -17,6 +17,7 @@ , "record" , "strings" , "tuples" + , "variant" ] , packages = ./packages.dhall , sources = [ "src/**/*.purs", "test/**/*.purs" ] diff --git a/spago.lock b/spago.lock new file mode 100644 index 0000000..b5b1431 --- /dev/null +++ b/spago.lock @@ -0,0 +1,1840 @@ +{ + "workspace": { + "packages": { + "routing-duplex": { + "path": "./", + "core": { + "dependencies": [ + { + "arrays": ">=7.3.0 <8.0.0" + }, + { + "control": ">=6.0.0 <7.0.0" + }, + { + "either": ">=6.1.0 <7.0.0" + }, + { + "foldable-traversable": ">=6.0.0 <7.0.0" + }, + { + "integers": ">=6.0.0 <7.0.0" + }, + { + "js-uri": ">=3.1.0 <4.0.0" + }, + { + "lazy": ">=6.0.0 <7.0.0" + }, + { + "maybe": ">=6.0.0 <7.0.0" + }, + { + "newtype": ">=5.0.0 <6.0.0" + }, + { + "prelude": ">=6.0.2 <7.0.0" + }, + { + "profunctor": ">=6.0.1 <7.0.0" + }, + { + "record": ">=4.0.0 <5.0.0" + }, + { + "strings": ">=6.0.1 <7.0.0" + }, + { + "tuples": ">=7.0.0 <8.0.0" + }, + { + "unsafe-coerce": ">=6.0.0 <7.0.0" + }, + { + "variant": ">=8.0.0 <9.0.0" + } + ], + "build_plan": [ + "arrays", + "bifunctors", + "const", + "contravariant", + "control", + "distributive", + "effect", + "either", + "enums", + "exists", + "foldable-traversable", + "functions", + "functors", + "gen", + "identity", + "integers", + "invariant", + "js-uri", + "lazy", + "lists", + "maybe", + "newtype", + "nonempty", + "numbers", + "orders", + "partial", + "prelude", + "profunctor", + "record", + "refs", + "safe-coerce", + "st", + "strings", + "tailrec", + "tuples", + "type-equality", + "unfoldable", + "unsafe-coerce", + "variant" + ] + }, + "test": { + "dependencies": [ + "effect", + "quickcheck", + "spec", + "spec-node", + "spec-quickcheck", + "variant-gen" + ], + "build_plan": [ + "aff", + "ansi", + "argonaut-codecs", + "argonaut-core", + "arraybuffer-types", + "arrays", + "avar", + "bifunctors", + "catenable-lists", + "console", + "const", + "contravariant", + "control", + "datetime", + "distributive", + "effect", + "either", + "enums", + "exceptions", + "exists", + "exitcodes", + "foldable-traversable", + "foreign", + "foreign-object", + "fork", + "free", + "functions", + "functors", + "gen", + "identity", + "integers", + "invariant", + "js-date", + "lazy", + "lcg", + "lists", + "maybe", + "mmorph", + "newtype", + "node-buffer", + "node-event-emitter", + "node-fs", + "node-path", + "node-process", + "node-streams", + "nonempty", + "now", + "nullable", + "numbers", + "open-memoize", + "optparse", + "ordered-collections", + "orders", + "parallel", + "partial", + "pipes", + "posix-types", + "prelude", + "profunctor", + "quickcheck", + "random", + "record", + "refs", + "safe-coerce", + "spec", + "spec-node", + "spec-quickcheck", + "st", + "strings", + "tailrec", + "transformers", + "tuples", + "type-equality", + "typelevel-prelude", + "unfoldable", + "unsafe-coerce", + "variant", + "variant-gen" + ] + } + } + }, + "package_set": { + "address": { + "registry": "64.3.0" + }, + "compiler": ">=0.15.15 <0.16.0", + "content": { + "abc-parser": "2.0.1", + "ace": "9.1.0", + "address-rfc2821": "0.1.1", + "aff": "8.0.0", + "aff-bus": "6.0.0", + "aff-coroutines": "9.0.0", + "aff-promise": "4.0.0", + "aff-retry": "2.0.0", + "affjax": "13.0.0", + "affjax-node": "1.0.0", + "affjax-web": "1.0.0", + "ansi": "7.0.0", + "apexcharts": "0.5.0", + "applicative-phases": "1.0.0", + "argonaut": "9.0.0", + "argonaut-aeson-generic": "0.4.1", + "argonaut-codecs": "9.1.0", + "argonaut-core": "7.0.0", + "argonaut-generic": "8.0.0", + "argonaut-traversals": "10.0.0", + "argparse-basic": "2.0.0", + "array-builder": "0.1.2", + "array-search": "0.6.0", + "arraybuffer": "13.2.0", + "arraybuffer-builder": "3.1.0", + "arraybuffer-types": "3.0.2", + "arrays": "7.3.0", + "arrays-extra": "0.6.1", + "arrays-zipper": "2.0.1", + "ask": "1.0.0", + "assert": "6.0.0", + "assert-multiple": "0.4.0", + "avar": "5.0.1", + "b64": "0.0.8", + "barbies": "1.0.1", + "barlow-lens": "0.9.0", + "benchlib": "0.0.4", + "bifunctors": "6.1.0", + "bigints": "7.0.1", + "bolson": "0.3.9", + "bookhound": "0.1.7", + "bower-json": "3.0.0", + "call-by-name": "4.0.1", + "canvas": "6.0.0", + "canvas-action": "9.0.0", + "cartesian": "1.0.6", + "catenable-lists": "7.0.0", + "cbor-stream": "1.3.0", + "chameleon": "1.0.0", + "chameleon-halogen": "1.0.3", + "chameleon-react-basic": "1.1.0", + "chameleon-styled": "2.5.0", + "chameleon-transformers": "1.0.0", + "channel": "1.0.0", + "checked-exceptions": "3.1.1", + "choku": "1.0.2", + "classless": "0.1.1", + "classless-arbitrary": "0.1.1", + "classless-decode-json": "0.1.1", + "classless-encode-json": "0.1.3", + "classnames": "2.0.0", + "codec": "6.1.0", + "codec-argonaut": "10.0.0", + "codec-json": "2.0.0", + "colors": "7.0.1", + "concur-core": "0.5.0", + "concur-react": "0.5.0", + "concurrent-queues": "3.0.0", + "console": "6.1.0", + "const": "6.0.0", + "contravariant": "6.0.0", + "control": "6.0.0", + "convertable-options": "1.0.0", + "coroutines": "7.0.0", + "css": "6.0.0", + "css-class-name-extractor": "0.0.4", + "css-frameworks": "1.0.1", + "csv-stream": "2.3.0", + "data-mvc": "0.0.2", + "datetime": "6.1.0", + "datetime-parsing": "0.2.0", + "debounce": "0.1.0", + "debug": "6.0.2", + "decimals": "7.1.0", + "default-values": "1.0.1", + "deku": "0.9.23", + "deno": "0.0.5", + "dissect": "1.0.0", + "distributive": "6.0.0", + "dodo-printer": "2.2.3", + "dom-filereader": "7.0.0", + "dom-indexed": "12.0.0", + "dom-simple": "0.4.0", + "dotenv": "4.0.3", + "droplet": "0.6.0", + "dts": "1.0.0", + "dual-numbers": "1.0.3", + "dynamic-buffer": "3.0.1", + "echarts-simple": "0.0.1", + "effect": "4.0.0", + "either": "6.1.0", + "elmish": "0.13.0", + "elmish-enzyme": "0.1.1", + "elmish-hooks": "0.10.3", + "elmish-html": "0.9.0", + "elmish-testing-library": "0.3.2", + "email-validate": "7.0.0", + "encoding": "0.0.9", + "enums": "6.0.1", + "env-names": "0.4.0", + "error": "2.0.0", + "eta-conversion": "0.3.2", + "exceptions": "6.1.0", + "exists": "6.0.0", + "exitcodes": "4.0.0", + "expect-inferred": "3.0.0", + "ezfetch": "1.1.0", + "fahrtwind": "2.0.0", + "fallback": "0.1.0", + "fast-vect": "1.2.0", + "fetch": "4.1.0", + "fetch-argonaut": "1.0.1", + "fetch-core": "5.1.0", + "fetch-yoga-json": "1.1.0", + "ffi-simple": "0.5.1", + "fft-js": "0.1.0", + "filterable": "5.0.0", + "fix-functor": "0.1.0", + "fixed-points": "7.0.0", + "fixed-precision": "5.0.0", + "flame": "1.3.0", + "float32": "2.0.0", + "fmt": "0.2.1", + "foldable-traversable": "6.0.0", + "foldable-traversable-extra": "0.0.6", + "foreign": "7.0.0", + "foreign-object": "4.1.0", + "foreign-readwrite": "3.4.0", + "forgetmenot": "0.1.0", + "fork": "6.0.0", + "form-urlencoded": "7.0.0", + "formatters": "7.0.0", + "framer-motion": "1.0.1", + "free": "7.1.0", + "freeap": "7.0.0", + "freer-free": "0.0.1", + "freet": "7.0.0", + "functions": "6.0.0", + "functor1": "3.0.0", + "functors": "5.0.0", + "fuzzy": "0.4.0", + "gen": "4.0.0", + "generate-values": "1.0.1", + "generic-router": "0.0.1", + "geojson": "0.0.5", + "geometria": "2.2.0", + "gesso": "1.0.0", + "gojs": "0.1.1", + "grain": "3.0.0", + "grain-router": "3.0.0", + "grain-virtualized": "3.0.0", + "graphs": "8.1.0", + "group": "4.1.1", + "halogen": "7.0.0", + "halogen-bootstrap5": "5.3.2", + "halogen-canvas": "1.0.0", + "halogen-css": "10.0.0", + "halogen-declarative-canvas": "0.0.8", + "halogen-echarts-simple": "0.0.4", + "halogen-formless": "4.0.3", + "halogen-helix": "1.1.0", + "halogen-hooks": "0.6.3", + "halogen-hooks-extra": "0.9.0", + "halogen-infinite-scroll": "1.1.0", + "halogen-store": "0.5.4", + "halogen-storybook": "2.0.0", + "halogen-subscriptions": "2.0.0", + "halogen-svg-elems": "8.0.0", + "halogen-typewriter": "1.0.4", + "halogen-use-trigger-hooks": "1.0.0", + "halogen-vdom": "8.0.0", + "halogen-vdom-string-renderer": "0.5.0", + "halogen-xterm": "2.0.0", + "heckin": "2.0.1", + "heterogeneous": "0.6.0", + "homogeneous": "0.4.0", + "http-methods": "6.0.0", + "httpurple": "4.0.0", + "huffman": "0.4.0", + "humdrum": "0.0.1", + "hyrule": "2.3.8", + "identity": "6.0.0", + "identy": "4.0.1", + "indexed-db": "1.0.0", + "indexed-monad": "3.0.0", + "int64": "3.0.0", + "integers": "6.0.0", + "interpolate": "5.0.2", + "intersection-observer": "1.0.1", + "invariant": "6.0.0", + "jarilo": "1.0.1", + "jelly": "0.10.0", + "jelly-router": "0.3.0", + "jelly-signal": "0.4.0", + "jest": "1.0.0", + "js-abort-controller": "1.0.0", + "js-bigints": "2.2.1", + "js-date": "8.0.0", + "js-fetch": "0.2.1", + "js-fileio": "3.0.0", + "js-intl": "1.1.4", + "js-iterators": "0.1.1", + "js-maps": "0.1.2", + "js-promise": "1.0.0", + "js-promise-aff": "1.0.0", + "js-timers": "6.1.0", + "js-uri": "3.1.0", + "jsdom": "1.0.0", + "json": "1.1.0", + "json-codecs": "5.0.0", + "justifill": "0.5.0", + "jwt": "0.0.9", + "labeled-data": "0.2.0", + "language-cst-parser": "0.14.1", + "lazy": "6.0.0", + "lazy-joe": "1.0.0", + "lcg": "4.0.0", + "leibniz": "5.0.0", + "leveldb": "1.0.1", + "liminal": "1.0.1", + "linalg": "6.0.0", + "lists": "7.0.0", + "literals": "1.0.2", + "logging": "3.0.0", + "logging-journald": "0.4.0", + "lumi-components": "18.0.0", + "machines": "7.0.0", + "maps-eager": "0.5.0", + "marionette": "1.0.0", + "marionette-react-basic-hooks": "0.1.1", + "marked": "0.1.0", + "matrices": "5.0.1", + "matryoshka": "1.0.0", + "maybe": "6.0.0", + "media-types": "6.0.0", + "meowclient": "1.0.0", + "midi": "4.0.0", + "milkis": "9.0.0", + "minibench": "4.0.1", + "mmorph": "7.0.0", + "monad-control": "5.0.0", + "monad-logger": "1.3.1", + "monad-loops": "0.5.0", + "monad-unlift": "1.0.1", + "monoid-extras": "0.0.1", + "monoidal": "0.16.0", + "morello": "0.4.0", + "mote": "3.0.0", + "motsunabe": "2.0.0", + "mvc": "0.0.1", + "mysql": "6.0.1", + "n3": "0.1.0", + "nano-id": "1.1.0", + "nanoid": "0.1.0", + "naturals": "3.0.0", + "nested-functor": "0.2.1", + "newtype": "5.0.0", + "nextjs": "0.1.1", + "nextui": "0.2.0", + "node-buffer": "9.0.0", + "node-child-process": "11.1.0", + "node-event-emitter": "3.0.0", + "node-execa": "5.0.0", + "node-fs": "9.2.0", + "node-glob-basic": "2.0.0", + "node-http": "9.1.0", + "node-http2": "1.1.1", + "node-human-signals": "1.0.0", + "node-net": "5.1.0", + "node-os": "5.1.0", + "node-path": "5.0.1", + "node-process": "11.2.0", + "node-readline": "8.1.1", + "node-sqlite3": "8.0.0", + "node-stream-pipes": "2.1.6", + "node-streams": "9.0.1", + "node-tls": "0.3.1", + "node-url": "7.0.1", + "node-workerbees": "0.3.1", + "node-zlib": "0.4.0", + "nonempty": "7.0.0", + "now": "6.0.0", + "npm-package-json": "2.0.0", + "nullable": "6.0.0", + "numberfield": "0.2.2", + "numbers": "9.0.1", + "oak": "3.1.1", + "oak-debug": "1.2.2", + "object-maps": "0.3.0", + "ocarina": "1.5.4", + "oooooooooorrrrrrrmm-lib": "0.0.1", + "open-colors-scales-and-schemes": "1.0.0", + "open-folds": "6.4.0", + "open-foreign-generic": "11.0.3", + "open-memoize": "6.2.0", + "open-mkdirp-aff": "1.2.0", + "open-pairing": "6.2.0", + "open-smolder": "12.0.2", + "options": "7.0.0", + "optparse": "5.0.1", + "ordered-collections": "3.2.0", + "ordered-set": "0.4.0", + "orders": "6.0.0", + "owoify": "1.2.0", + "pairs": "9.0.1", + "parallel": "7.0.0", + "parsing": "10.3.0", + "parsing-dataview": "3.2.4", + "partial": "4.0.0", + "pathy": "9.0.0", + "pha": "0.13.0", + "phaser": "0.7.0", + "phylio": "1.1.2", + "pipes": "8.0.0", + "pirates-charm": "0.0.1", + "pmock": "0.9.0", + "point-free": "1.0.0", + "pointed-list": "0.5.1", + "polymorphic-vectors": "4.0.0", + "posix-types": "6.0.0", + "postgresql": "2.0.20", + "precise": "6.0.0", + "precise-datetime": "7.0.0", + "prelude": "6.0.2", + "prettier-printer": "3.0.0", + "printf": "0.1.0", + "priority-queue": "0.1.2", + "profunctor": "6.0.1", + "profunctor-lenses": "8.0.0", + "protobuf": "4.4.0", + "psa-utils": "8.0.0", + "psci-support": "6.0.0", + "punycode": "1.0.0", + "qualified-do": "2.2.0", + "quantities": "12.2.0", + "quickcheck": "8.0.1", + "quickcheck-combinators": "0.1.3", + "quickcheck-laws": "7.0.0", + "quickcheck-utf8": "0.0.0", + "random": "6.0.0", + "rationals": "6.0.0", + "rdf": "0.1.0", + "react": "11.0.0", + "react-aria": "0.2.0", + "react-basic": "17.0.0", + "react-basic-classic": "3.0.0", + "react-basic-dnd": "10.1.0", + "react-basic-dom": "7.0.0", + "react-basic-dom-beta": "0.1.1", + "react-basic-emotion": "7.1.0", + "react-basic-hooks": "8.2.0", + "react-basic-storybook": "2.0.0", + "react-dom": "8.0.0", + "react-halo": "3.0.0", + "react-icons": "1.1.5", + "react-markdown": "0.1.0", + "react-testing-library": "4.0.1", + "react-virtuoso": "1.0.0", + "reactix": "0.6.1", + "read": "1.0.1", + "recharts": "1.1.0", + "record": "4.0.0", + "record-extra": "5.0.1", + "record-extra-srghma": "0.1.1", + "record-ptional-fields": "0.1.2", + "record-studio": "1.0.4", + "refs": "6.0.0", + "remotedata": "5.0.1", + "repr": "0.5.0", + "resize-arrays": "0.0.1", + "resize-observer": "1.0.0", + "resource": "2.0.1", + "resourcet": "1.0.0", + "result": "1.0.3", + "return": "0.2.0", + "ring-modules": "5.0.1", + "rito": "0.3.4", + "roman": "0.4.0", + "rough-notation": "1.0.2", + "routing": "11.0.0", + "routing-duplex": "0.7.0", + "run": "5.0.0", + "safe-coerce": "2.0.0", + "safely": "4.0.1", + "school-of-music": "1.3.0", + "selection-foldable": "0.2.0", + "selective-functors": "1.0.1", + "semirings": "7.0.0", + "shuffle": "1.1.0", + "signal": "13.0.0", + "simple-emitter": "3.0.1", + "simple-i18n": "2.0.1", + "simple-json": "9.0.0", + "simple-json-generics": "0.2.1", + "simple-ulid": "3.0.0", + "sized-matrices": "1.0.0", + "sized-vectors": "5.0.2", + "slug": "3.1.0", + "small-ffi": "4.0.1", + "soundfonts": "4.1.0", + "sparse-matrices": "2.0.1", + "sparse-polynomials": "3.0.1", + "spec": "8.1.1", + "spec-discovery": "8.4.0", + "spec-mocha": "5.1.1", + "spec-node": "0.0.3", + "spec-quickcheck": "5.0.2", + "spec-reporter-xunit": "0.7.1", + "splitmix": "2.1.0", + "ssrs": "1.0.0", + "st": "6.2.0", + "statistics": "0.3.2", + "strictlypositiveint": "1.0.1", + "string-parsers": "8.0.0", + "strings": "6.0.1", + "strings-extra": "4.0.0", + "stringutils": "0.0.12", + "substitute": "0.2.3", + "supply": "0.2.0", + "svg-parser": "3.0.0", + "systemd-journald": "0.3.0", + "tagged": "4.0.2", + "tailrec": "6.1.0", + "tanstack-query": "2.0.0", + "tecton": "0.2.1", + "tecton-halogen": "0.2.0", + "test-unit": "17.0.0", + "thermite": "6.3.1", + "thermite-dom": "0.3.1", + "these": "6.0.0", + "threading": "0.0.3", + "tidy": "0.11.1", + "tidy-codegen": "4.0.1", + "tldr": "0.0.0", + "toestand": "0.9.0", + "transformation-matrix": "1.0.1", + "transformers": "6.1.0", + "tree-rose": "4.0.2", + "trivial-unfold": "0.5.0", + "ts-bridge": "4.0.0", + "tuples": "7.0.0", + "two-or-more": "1.0.0", + "type-equality": "4.0.1", + "typedenv": "2.0.1", + "typelevel": "6.0.0", + "typelevel-lists": "2.1.0", + "typelevel-peano": "1.0.1", + "typelevel-prelude": "7.0.0", + "typelevel-regex": "0.0.3", + "typelevel-rows": "0.1.0", + "typisch": "0.4.0", + "uint": "7.0.0", + "ulid": "3.0.1", + "uncurried-transformers": "1.1.0", + "undefined": "2.0.0", + "undefined-is-not-a-problem": "1.1.0", + "unfoldable": "6.0.0", + "unicode": "6.0.0", + "unique": "0.6.1", + "unlift": "1.0.1", + "unordered-collections": "3.1.0", + "unsafe-coerce": "6.0.0", + "unsafe-reference": "5.0.0", + "untagged-to-tagged": "0.1.4", + "untagged-union": "1.0.0", + "uri": "9.0.0", + "url-immutable": "1.0.0", + "url-regex-safe": "0.1.1", + "uuid": "9.0.0", + "uuidv4": "1.0.0", + "validation": "6.0.0", + "variant": "8.0.0", + "variant-encodings": "2.0.0", + "vectorfield": "1.0.1", + "vectors": "2.1.0", + "versions": "7.0.0", + "visx": "0.0.2", + "vitest": "1.0.0", + "web-clipboard": "6.0.0", + "web-cssom": "2.0.0", + "web-cssom-view": "0.1.0", + "web-dom": "6.0.0", + "web-dom-parser": "8.0.0", + "web-dom-xpath": "3.0.0", + "web-encoding": "3.0.0", + "web-events": "4.0.0", + "web-fetch": "4.0.1", + "web-file": "4.0.0", + "web-geometry": "0.1.0", + "web-html": "4.1.0", + "web-pointerevents": "2.0.0", + "web-proletarian": "1.0.0", + "web-promise": "3.2.0", + "web-resize-observer": "2.1.0", + "web-router": "1.0.0", + "web-socket": "4.0.0", + "web-storage": "5.0.0", + "web-streams": "4.0.0", + "web-touchevents": "4.0.0", + "web-uievents": "5.0.0", + "web-url": "2.0.0", + "web-workers": "1.1.0", + "web-xhr": "5.0.1", + "webextension-polyfill": "0.1.0", + "webgpu": "0.0.1", + "which": "2.0.0", + "whine-core": "0.0.28", + "xterm": "1.0.0", + "yoga-fetch": "1.0.1", + "yoga-json": "5.1.0", + "yoga-om": "0.1.0", + "yoga-postgres": "6.0.0", + "yoga-react-dom": "1.0.1", + "yoga-subtlecrypto": "0.1.0", + "yoga-tree": "1.0.0", + "z3": "0.0.2", + "zipperarray": "2.0.0" + } + }, + "extra_packages": { + "variant-gen": { + "git": "https://github.com/purescript-open-community/purescript-variant-gen/", + "ref": "main" + } + } + }, + "packages": { + "aff": { + "type": "registry", + "version": "8.0.0", + "integrity": "sha256-5MmdI4+0RHBtSBy+YlU3/Cq4R5W2ih3OaRedJIrVHdk=", + "dependencies": [ + "bifunctors", + "control", + "datetime", + "effect", + "either", + "exceptions", + "foldable-traversable", + "functions", + "maybe", + "newtype", + "parallel", + "prelude", + "refs", + "tailrec", + "transformers", + "unsafe-coerce" + ] + }, + "ansi": { + "type": "registry", + "version": "7.0.0", + "integrity": "sha256-ZMB6HD+q9CXvn9fRCmJ8dvuDrOVHcjombL3oNOerVnE=", + "dependencies": [ + "foldable-traversable", + "lists", + "strings" + ] + }, + "argonaut-codecs": { + "type": "registry", + "version": "9.1.0", + "integrity": "sha256-N6efXByUeg848ompEqJfVvZuZPfdRYDGlTDFn0G0Oh8=", + "dependencies": [ + "argonaut-core", + "arrays", + "effect", + "foreign-object", + "identity", + "integers", + "maybe", + "nonempty", + "ordered-collections", + "prelude", + "record" + ] + }, + "argonaut-core": { + "type": "registry", + "version": "7.0.0", + "integrity": "sha256-RC82GfAjItydxrO24cdX373KHVZiLqybu19b5X8u7B4=", + "dependencies": [ + "arrays", + "control", + "either", + "foreign-object", + "functions", + "gen", + "maybe", + "nonempty", + "prelude", + "strings", + "tailrec" + ] + }, + "arraybuffer-types": { + "type": "registry", + "version": "3.0.2", + "integrity": "sha256-mQKokysYVkooS4uXbO+yovmV/s8b138Ws3zQvOwIHRA=", + "dependencies": [] + }, + "arrays": { + "type": "registry", + "version": "7.3.0", + "integrity": "sha256-tmcklBlc/muUtUfr9RapdCPwnlQeB3aSrC4dK85gQlc=", + "dependencies": [ + "bifunctors", + "control", + "foldable-traversable", + "functions", + "maybe", + "nonempty", + "partial", + "prelude", + "safe-coerce", + "st", + "tailrec", + "tuples", + "unfoldable", + "unsafe-coerce" + ] + }, + "avar": { + "type": "registry", + "version": "5.0.1", + "integrity": "sha256-f+bRR3qQPa/GVe4UbLQiJBy7+PzJkUCwT6qNn0UlkMY=", + "dependencies": [ + "aff", + "effect", + "either", + "exceptions", + "functions", + "maybe" + ] + }, + "bifunctors": { + "type": "registry", + "version": "6.1.0", + "integrity": "sha256-6enQzl1vqnFTQZ1WX9BnoOOVdPGO9WZvVXldHckVQvY=", + "dependencies": [ + "const", + "either", + "newtype", + "prelude", + "tuples" + ] + }, + "catenable-lists": { + "type": "registry", + "version": "7.0.0", + "integrity": "sha256-76vYENhwF4BWTBsjeLuErCH2jqVT4M3R1HX+4RwSftA=", + "dependencies": [ + "control", + "foldable-traversable", + "lists", + "maybe", + "prelude", + "tuples", + "unfoldable" + ] + }, + "console": { + "type": "registry", + "version": "6.1.0", + "integrity": "sha256-CxmAzjgyuGDmt9FZW51VhV6rBPwR6o0YeKUzA9rSzcM=", + "dependencies": [ + "effect", + "prelude" + ] + }, + "const": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-tNrxDW8D8H4jdHE2HiPzpLy08zkzJMmGHdRqt5BQuTc=", + "dependencies": [ + "invariant", + "newtype", + "prelude" + ] + }, + "contravariant": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-TP+ooAp3vvmdjfQsQJSichF5B4BPDHp3wAJoWchip6c=", + "dependencies": [ + "const", + "either", + "newtype", + "prelude", + "tuples" + ] + }, + "control": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-sH7Pg9E96JCPF9PIA6oQ8+BjTyO/BH1ZuE/bOcyj4Jk=", + "dependencies": [ + "newtype", + "prelude" + ] + }, + "datetime": { + "type": "registry", + "version": "6.1.0", + "integrity": "sha256-g/5X5BBegQWLpI9IWD+sY6mcaYpzzlW5lz5NBzaMtyI=", + "dependencies": [ + "bifunctors", + "control", + "either", + "enums", + "foldable-traversable", + "functions", + "gen", + "integers", + "lists", + "maybe", + "newtype", + "numbers", + "ordered-collections", + "partial", + "prelude", + "tuples" + ] + }, + "distributive": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-HTDdmEnzigMl+02SJB88j+gAXDx9VKsbvR4MJGDPbOQ=", + "dependencies": [ + "identity", + "newtype", + "prelude", + "tuples", + "type-equality" + ] + }, + "effect": { + "type": "registry", + "version": "4.0.0", + "integrity": "sha256-eBtZu+HZcMa5HilvI6kaDyVX3ji8p0W9MGKy2K4T6+M=", + "dependencies": [ + "prelude" + ] + }, + "either": { + "type": "registry", + "version": "6.1.0", + "integrity": "sha256-6hgTPisnMWVwQivOu2PKYcH8uqjEOOqDyaDQVUchTpY=", + "dependencies": [ + "control", + "invariant", + "maybe", + "prelude" + ] + }, + "enums": { + "type": "registry", + "version": "6.0.1", + "integrity": "sha256-HWaD73JFLorc4A6trKIRUeDMdzE+GpkJaEOM1nTNkC8=", + "dependencies": [ + "control", + "either", + "gen", + "maybe", + "newtype", + "nonempty", + "partial", + "prelude", + "tuples", + "unfoldable" + ] + }, + "exceptions": { + "type": "registry", + "version": "6.1.0", + "integrity": "sha256-K0T89IHtF3vBY7eSAO7eDOqSb2J9kZGAcDN5+IKsF8E=", + "dependencies": [ + "effect", + "either", + "maybe", + "prelude" + ] + }, + "exists": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-A0JQHpTfo1dNOj9U5/Fd3xndlRSE0g2IQWOGor2yXn8=", + "dependencies": [ + "unsafe-coerce" + ] + }, + "exitcodes": { + "type": "registry", + "version": "4.0.0", + "integrity": "sha256-4wxViTbyOoyKJ/WaRGI6+hZmgMKI5Miv16lSwefiLSM=", + "dependencies": [ + "enums" + ] + }, + "foldable-traversable": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-fLeqRYM4jUrZD5H4WqcwUgzU7XfYkzO4zhgtNc3jcWM=", + "dependencies": [ + "bifunctors", + "const", + "control", + "either", + "functors", + "identity", + "maybe", + "newtype", + "orders", + "prelude", + "tuples" + ] + }, + "foreign": { + "type": "registry", + "version": "7.0.0", + "integrity": "sha256-1ORiqoS3HW+qfwSZAppHPWy4/6AQysxZ2t29jcdUMNA=", + "dependencies": [ + "either", + "functions", + "identity", + "integers", + "lists", + "maybe", + "prelude", + "strings", + "transformers" + ] + }, + "foreign-object": { + "type": "registry", + "version": "4.1.0", + "integrity": "sha256-q24okj6mT+yGHYQ+ei/pYPj5ih6sTbu7eDv/WU56JVo=", + "dependencies": [ + "arrays", + "foldable-traversable", + "functions", + "gen", + "lists", + "maybe", + "prelude", + "st", + "tailrec", + "tuples", + "typelevel-prelude", + "unfoldable" + ] + }, + "fork": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-X7u0SuCvFbLbzuNEKLBNuWjmcroqMqit4xEzpQwAP7E=", + "dependencies": [ + "aff" + ] + }, + "free": { + "type": "registry", + "version": "7.1.0", + "integrity": "sha256-JAumgEsGSzJCNLD8AaFvuX7CpqS5yruCngi6yI7+V5k=", + "dependencies": [ + "catenable-lists", + "control", + "distributive", + "either", + "exists", + "foldable-traversable", + "invariant", + "lazy", + "maybe", + "prelude", + "tailrec", + "transformers", + "tuples", + "unsafe-coerce" + ] + }, + "functions": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-adMyJNEnhGde2unHHAP79gPtlNjNqzgLB8arEOn9hLI=", + "dependencies": [ + "prelude" + ] + }, + "functors": { + "type": "registry", + "version": "5.0.0", + "integrity": "sha256-zfPWWYisbD84MqwpJSZFlvM6v86McM68ob8p9s27ywU=", + "dependencies": [ + "bifunctors", + "const", + "contravariant", + "control", + "distributive", + "either", + "invariant", + "maybe", + "newtype", + "prelude", + "profunctor", + "tuples", + "unsafe-coerce" + ] + }, + "gen": { + "type": "registry", + "version": "4.0.0", + "integrity": "sha256-f7yzAXWwr+xnaqEOcvyO3ezKdoes8+WXWdXIHDBCAPI=", + "dependencies": [ + "either", + "foldable-traversable", + "identity", + "maybe", + "newtype", + "nonempty", + "prelude", + "tailrec", + "tuples", + "unfoldable" + ] + }, + "identity": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-4wY0XZbAksjY6UAg99WkuKyJlQlWAfTi2ssadH0wVMY=", + "dependencies": [ + "control", + "invariant", + "newtype", + "prelude" + ] + }, + "integers": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-sf+sK26R1hzwl3NhXR7WAu9zCDjQnfoXwcyGoseX158=", + "dependencies": [ + "maybe", + "numbers", + "prelude" + ] + }, + "invariant": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-RGWWyYrz0Hs1KjPDA+87Kia67ZFBhfJ5lMGOMCEFoLo=", + "dependencies": [ + "control", + "prelude" + ] + }, + "js-date": { + "type": "registry", + "version": "8.0.0", + "integrity": "sha256-6TVF4DWg5JL+jRAsoMssYw8rgOVALMUHT1CuNZt8NRo=", + "dependencies": [ + "datetime", + "effect", + "exceptions", + "foreign", + "integers", + "now" + ] + }, + "js-uri": { + "type": "registry", + "version": "3.1.0", + "integrity": "sha256-3p0ynHveCJmC2CXze+eMBdW/2l5e953Q8XMAKz+jxUo=", + "dependencies": [ + "functions", + "maybe" + ] + }, + "lazy": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-lMsfFOnlqfe4KzRRiW8ot5ge6HtcU3Eyh2XkXcP5IgU=", + "dependencies": [ + "control", + "foldable-traversable", + "invariant", + "prelude" + ] + }, + "lcg": { + "type": "registry", + "version": "4.0.0", + "integrity": "sha256-h7ME5cthLfbgJOJdsZcSfFpwXsx4rf8YmhebU+3iSYg=", + "dependencies": [ + "effect", + "integers", + "maybe", + "partial", + "prelude", + "random" + ] + }, + "lists": { + "type": "registry", + "version": "7.0.0", + "integrity": "sha256-EKF15qYqucuXP2lT/xPxhqy58f0FFT6KHdIB/yBOayI=", + "dependencies": [ + "bifunctors", + "control", + "foldable-traversable", + "lazy", + "maybe", + "newtype", + "nonempty", + "partial", + "prelude", + "tailrec", + "tuples", + "unfoldable" + ] + }, + "maybe": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-5cCIb0wPwbat2PRkQhUeZO0jcAmf8jCt2qE0wbC3v2Q=", + "dependencies": [ + "control", + "invariant", + "newtype", + "prelude" + ] + }, + "mmorph": { + "type": "registry", + "version": "7.0.0", + "integrity": "sha256-urZlZNNqGeQFe5D/ClHlR8QgGBNHTMFPtJ5S5IpflTQ=", + "dependencies": [ + "free", + "functors", + "transformers" + ] + }, + "newtype": { + "type": "registry", + "version": "5.0.0", + "integrity": "sha256-gdrQu8oGe9eZE6L3wOI8ql/igOg+zEGB5ITh2g+uttw=", + "dependencies": [ + "prelude", + "safe-coerce" + ] + }, + "node-buffer": { + "type": "registry", + "version": "9.0.0", + "integrity": "sha256-PWE2DJ5ruBLCmeA/fUiuySEFmUJ/VuRfyrnCuVZBlu4=", + "dependencies": [ + "arraybuffer-types", + "effect", + "maybe", + "nullable", + "st", + "unsafe-coerce" + ] + }, + "node-event-emitter": { + "type": "registry", + "version": "3.0.0", + "integrity": "sha256-Qw0MjsT4xRH2j2i4K8JmRjcMKnH5z1Cw39t00q4LE4w=", + "dependencies": [ + "effect", + "either", + "functions", + "maybe", + "nullable", + "prelude", + "unsafe-coerce" + ] + }, + "node-fs": { + "type": "registry", + "version": "9.2.0", + "integrity": "sha256-Sg0vkXycEzkEerX6hLccz21Ygd9w1+QSk1thotRZPGI=", + "dependencies": [ + "datetime", + "effect", + "either", + "enums", + "exceptions", + "functions", + "integers", + "js-date", + "maybe", + "node-buffer", + "node-path", + "node-streams", + "nullable", + "partial", + "prelude", + "strings", + "unsafe-coerce" + ] + }, + "node-path": { + "type": "registry", + "version": "5.0.1", + "integrity": "sha256-ePOElFamHkffhwJcS0Ozq4A14rflnkasFU6X2B8/yXs=", + "dependencies": [ + "effect" + ] + }, + "node-process": { + "type": "registry", + "version": "11.2.0", + "integrity": "sha256-+2MQDYChjGbVbapCyJtuWYwD41jk+BntF/kcOTKBMVs=", + "dependencies": [ + "effect", + "foreign", + "foreign-object", + "maybe", + "node-event-emitter", + "node-streams", + "posix-types", + "prelude", + "unsafe-coerce" + ] + }, + "node-streams": { + "type": "registry", + "version": "9.0.1", + "integrity": "sha256-7RJ6RqjOlhW+QlDFQNUHlkCG/CuYTTLT8yary5jhhsU=", + "dependencies": [ + "aff", + "arrays", + "effect", + "either", + "exceptions", + "maybe", + "node-buffer", + "node-event-emitter", + "nullable", + "prelude", + "refs", + "st", + "tailrec", + "unsafe-coerce" + ] + }, + "nonempty": { + "type": "registry", + "version": "7.0.0", + "integrity": "sha256-54ablJZUHGvvlTJzi3oXyPCuvY6zsrWJuH/dMJ/MFLs=", + "dependencies": [ + "control", + "foldable-traversable", + "maybe", + "prelude", + "tuples", + "unfoldable" + ] + }, + "now": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-xZ7x37ZMREfs6GCDw/h+FaKHV/3sPWmtqBZRGTxybQY=", + "dependencies": [ + "datetime", + "effect" + ] + }, + "nullable": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-yiGBVl3AD+Guy4kNWWeN+zl1gCiJK+oeIFtZtPCw4+o=", + "dependencies": [ + "effect", + "functions", + "maybe" + ] + }, + "numbers": { + "type": "registry", + "version": "9.0.1", + "integrity": "sha256-/9M6aeMDBdB4cwYDeJvLFprAHZ49EbtKQLIJsneXLIk=", + "dependencies": [ + "functions", + "maybe" + ] + }, + "open-memoize": { + "type": "registry", + "version": "6.2.0", + "integrity": "sha256-p1m7wF3aHQ80yUvqMs20OTMl496WS6YpKlmI2Nkg9j0=", + "dependencies": [ + "either", + "integers", + "lazy", + "lists", + "maybe", + "partial", + "prelude", + "strings", + "tuples" + ] + }, + "optparse": { + "type": "registry", + "version": "5.0.1", + "integrity": "sha256-cEzEkNW4q0gZlXl4z0zn+H2vs6l2UAp7NPHCsois73k=", + "dependencies": [ + "aff", + "arrays", + "bifunctors", + "console", + "control", + "effect", + "either", + "enums", + "exists", + "exitcodes", + "foldable-traversable", + "free", + "gen", + "integers", + "lazy", + "lists", + "maybe", + "newtype", + "node-buffer", + "node-process", + "node-streams", + "nonempty", + "numbers", + "open-memoize", + "partial", + "prelude", + "strings", + "tailrec", + "transformers", + "tuples" + ] + }, + "ordered-collections": { + "type": "registry", + "version": "3.2.0", + "integrity": "sha256-o9jqsj5rpJmMdoe/zyufWHFjYYFTTsJpgcuCnqCO6PM=", + "dependencies": [ + "arrays", + "foldable-traversable", + "gen", + "lists", + "maybe", + "partial", + "prelude", + "st", + "tailrec", + "tuples", + "unfoldable" + ] + }, + "orders": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-nBA0g3/ai0euH8q9pSbGqk53W2q6agm/dECZTHcoink=", + "dependencies": [ + "newtype", + "prelude" + ] + }, + "parallel": { + "type": "registry", + "version": "7.0.0", + "integrity": "sha256-gUC9i4Txnx9K9RcMLsjujbwZz6BB1bnE2MLvw4GIw5o=", + "dependencies": [ + "control", + "effect", + "either", + "foldable-traversable", + "functors", + "maybe", + "newtype", + "prelude", + "profunctor", + "refs", + "transformers" + ] + }, + "partial": { + "type": "registry", + "version": "4.0.0", + "integrity": "sha256-fwXerld6Xw1VkReh8yeQsdtLVrjfGiVuC5bA1Wyo/J4=", + "dependencies": [] + }, + "pipes": { + "type": "registry", + "version": "8.0.0", + "integrity": "sha256-kvfqGM4cPA/wCcBHbp5psouFw5dZGvku2462x7ZBwSY=", + "dependencies": [ + "aff", + "lists", + "mmorph", + "prelude", + "tailrec", + "transformers", + "tuples" + ] + }, + "posix-types": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-ZfFz8RR1lee/o/Prccyeut3Q+9tYd08mlR72sIh6GzA=", + "dependencies": [ + "maybe", + "prelude" + ] + }, + "prelude": { + "type": "registry", + "version": "6.0.2", + "integrity": "sha256-kiAPZxihtAel8uRiTNdccf4qylp/9J3jNkEHNAD0MsE=", + "dependencies": [] + }, + "profunctor": { + "type": "registry", + "version": "6.0.1", + "integrity": "sha256-E58hSYdJvF2Qjf9dnWLPlJKh2Z2fLfFLkQoYi16vsFk=", + "dependencies": [ + "control", + "distributive", + "either", + "exists", + "invariant", + "newtype", + "prelude", + "tuples" + ] + }, + "quickcheck": { + "type": "registry", + "version": "8.0.1", + "integrity": "sha256-ZvpccKQCvgslTXZCNmpYW4bUsFzhZd/kQUr2WmxFTGY=", + "dependencies": [ + "arrays", + "console", + "control", + "effect", + "either", + "enums", + "exceptions", + "foldable-traversable", + "gen", + "identity", + "integers", + "lazy", + "lcg", + "lists", + "maybe", + "newtype", + "nonempty", + "numbers", + "partial", + "prelude", + "record", + "st", + "strings", + "tailrec", + "transformers", + "tuples", + "unfoldable" + ] + }, + "random": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-CJ611a35MPCE7XQMp0rdC6MCn76znlhisiCRgboAG+Q=", + "dependencies": [ + "effect", + "integers" + ] + }, + "record": { + "type": "registry", + "version": "4.0.0", + "integrity": "sha256-Za5U85bTRJEfGK5Sk4hM41oXy84YQI0I8TL3WUn1Qzg=", + "dependencies": [ + "functions", + "prelude", + "unsafe-coerce" + ] + }, + "refs": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-Vgwne7jIbD3ZMoLNNETLT8Litw6lIYo3MfYNdtYWj9s=", + "dependencies": [ + "effect", + "prelude" + ] + }, + "safe-coerce": { + "type": "registry", + "version": "2.0.0", + "integrity": "sha256-a1ibQkiUcbODbLE/WAq7Ttbbh9ex+x33VCQ7GngKudU=", + "dependencies": [ + "unsafe-coerce" + ] + }, + "spec": { + "type": "registry", + "version": "8.1.1", + "integrity": "sha256-EM7UfQIaSgiw13LJ4ZASkfYmmRDKIlec3nYbGKFqGhk=", + "dependencies": [ + "aff", + "ansi", + "arrays", + "avar", + "bifunctors", + "control", + "datetime", + "effect", + "either", + "exceptions", + "foldable-traversable", + "fork", + "identity", + "integers", + "lists", + "maybe", + "newtype", + "now", + "ordered-collections", + "parallel", + "pipes", + "prelude", + "refs", + "strings", + "tailrec", + "transformers", + "tuples" + ] + }, + "spec-node": { + "type": "registry", + "version": "0.0.3", + "integrity": "sha256-Bjzg6l4uOfMN/FV0SKuT1Mm8eMP9sloLGVcY/0MeMnI=", + "dependencies": [ + "aff", + "argonaut-codecs", + "argonaut-core", + "arrays", + "control", + "datetime", + "effect", + "either", + "foldable-traversable", + "identity", + "integers", + "maybe", + "newtype", + "node-buffer", + "node-fs", + "node-process", + "now", + "numbers", + "optparse", + "ordered-collections", + "partial", + "prelude", + "spec", + "strings", + "tuples" + ] + }, + "spec-quickcheck": { + "type": "registry", + "version": "5.0.2", + "integrity": "sha256-Qn3ahQEgCskgQiUOWA5pfgUbbB5Jcv/EVuRN7D70K+s=", + "dependencies": [ + "aff", + "arrays", + "effect", + "foldable-traversable", + "lists", + "maybe", + "prelude", + "quickcheck", + "tuples" + ] + }, + "st": { + "type": "registry", + "version": "6.2.0", + "integrity": "sha256-z9X0WsOUlPwNx9GlCC+YccCyz8MejC8Wb0C4+9fiBRY=", + "dependencies": [ + "partial", + "prelude", + "tailrec", + "unsafe-coerce" + ] + }, + "strings": { + "type": "registry", + "version": "6.0.1", + "integrity": "sha256-WssD3DbX4OPzxSdjvRMX0yvc9+pS7n5gyPv5I2Trb7k=", + "dependencies": [ + "arrays", + "control", + "either", + "enums", + "foldable-traversable", + "gen", + "integers", + "maybe", + "newtype", + "nonempty", + "partial", + "prelude", + "tailrec", + "tuples", + "unfoldable", + "unsafe-coerce" + ] + }, + "tailrec": { + "type": "registry", + "version": "6.1.0", + "integrity": "sha256-Xx19ECVDRrDWpz9D2GxQHHV89vd61dnXxQm0IcYQHGk=", + "dependencies": [ + "bifunctors", + "effect", + "either", + "identity", + "maybe", + "partial", + "prelude", + "refs" + ] + }, + "transformers": { + "type": "registry", + "version": "6.1.0", + "integrity": "sha256-3Bm+Z6tsC/paG888XkywDngJ2JMos+JfOhRlkVfb7gI=", + "dependencies": [ + "control", + "distributive", + "effect", + "either", + "exceptions", + "foldable-traversable", + "identity", + "lazy", + "maybe", + "newtype", + "prelude", + "st", + "tailrec", + "tuples", + "unfoldable" + ] + }, + "tuples": { + "type": "registry", + "version": "7.0.0", + "integrity": "sha256-1rXgTomes9105BjgXqIw0FL6Fz1lqqUTLWOumhWec1M=", + "dependencies": [ + "control", + "invariant", + "prelude" + ] + }, + "type-equality": { + "type": "registry", + "version": "4.0.1", + "integrity": "sha256-Hs9D6Y71zFi/b+qu5NSbuadUQXe5iv5iWx0226vOHUw=", + "dependencies": [] + }, + "typelevel-prelude": { + "type": "registry", + "version": "7.0.0", + "integrity": "sha256-uFF2ph+vHcQpfPuPf2a3ukJDFmLhApmkpTMviHIWgJM=", + "dependencies": [ + "prelude", + "type-equality" + ] + }, + "unfoldable": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-JtikvJdktRap7vr/K4ITlxUX1QexpnqBq0G/InLr6eg=", + "dependencies": [ + "foldable-traversable", + "maybe", + "partial", + "prelude", + "tuples" + ] + }, + "unsafe-coerce": { + "type": "registry", + "version": "6.0.0", + "integrity": "sha256-IqIYW4Vkevn8sI+6aUwRGvd87tVL36BBeOr0cGAE7t0=", + "dependencies": [] + }, + "variant": { + "type": "registry", + "version": "8.0.0", + "integrity": "sha256-SR//zQDg2dnbB8ZHslcxieUkCeNlbMToapvmh9onTtw=", + "dependencies": [ + "enums", + "lists", + "maybe", + "partial", + "prelude", + "record", + "tuples", + "unsafe-coerce" + ] + }, + "variant-gen": { + "type": "git", + "url": "https://github.com/purescript-open-community/purescript-variant-gen/", + "rev": "069b5123fd01717f09663a40904bd4f112165bc9", + "dependencies": [ + "gen", + "lists", + "nonempty", + "prelude", + "record", + "tuples", + "variant" + ] + } + } +} diff --git a/spago.yaml b/spago.yaml new file mode 100644 index 0000000..8bd9d42 --- /dev/null +++ b/spago.yaml @@ -0,0 +1,41 @@ +package: + name: routing-duplex + publish: + license: MIT + version: 0.7.0 + location: + githubOwner: purescript-contrib + githubRepo: purescript-routing-duplex + dependencies: + - arrays: ">=7.3.0 <8.0.0" + - control: ">=6.0.0 <7.0.0" + - either: ">=6.1.0 <7.0.0" + - foldable-traversable: ">=6.0.0 <7.0.0" + - integers: ">=6.0.0 <7.0.0" + - js-uri: ">=3.1.0 <4.0.0" + - lazy: ">=6.0.0 <7.0.0" + - maybe: ">=6.0.0 <7.0.0" + - newtype: ">=5.0.0 <6.0.0" + - prelude: ">=6.0.2 <7.0.0" + - profunctor: ">=6.0.1 <7.0.0" + - record: ">=4.0.0 <5.0.0" + - strings: ">=6.0.1 <7.0.0" + - tuples: ">=7.0.0 <8.0.0" + - unsafe-coerce: ">=6.0.0 <7.0.0" + - variant: ">=8.0.0 <9.0.0" + test: + main: Test.Main + dependencies: + - effect + - quickcheck + - spec + - spec-node + - spec-quickcheck + - variant-gen +workspace: + packageSet: + registry: 64.3.0 + extraPackages: + variant-gen: + git: https://github.com/purescript-open-community/purescript-variant-gen/ + ref: main diff --git a/src/Routing/Duplex.purs b/src/Routing/Duplex.purs index d412681..62dd77f 100644 --- a/src/Routing/Duplex.purs +++ b/src/Routing/Duplex.purs @@ -24,15 +24,21 @@ module Routing.Duplex , record , prop , (:=) + , variant + , vcase + , vmatch + , vmatchCases + , (%=) , params , buildParams + , class RouteDuplexVariantMatchCases , class RouteDuplexParams , class RouteDuplexBuildParams ) where import Prelude -import Control.Alt (class Alt) +import Control.Alt (class Alt, (<|>)) import Control.Alternative (class Alternative) import Data.Either (Either) import Data.Foldable (class Foldable, foldMap, foldr) @@ -41,14 +47,17 @@ import Data.Profunctor (class Profunctor) import Data.String (Pattern(..)) import Data.String as String import Data.Symbol (class IsSymbol, reflectSymbol) +import Data.Variant (Variant) +import Data.Variant as Variant import Prim.Row as Row import Prim.RowList (class RowToList, Cons, Nil, RowList) import Record as Record -import Routing.Duplex.Parser (RouteParser) +import Routing.Duplex.Parser (RouteError(..), RouteParser(..), RouteResult(..)) import Routing.Duplex.Parser as Parser import Routing.Duplex.Printer (RoutePrinter) import Routing.Duplex.Printer as Printer import Type.Proxy (Proxy(..)) +import Unsafe.Coerce (unsafeCoerce) -- | The core abstraction of this library. The values of this type can be used both for parsing -- | values of type `o` from `String` as well as printing values of type `i` into `String`. @@ -339,6 +348,94 @@ prop sym (RouteDuplex f g) (RouteDuplex x y) = infix 2 prop as := +-- | Combined with `vcase`, builds a Variant where the order of parsing and +-- | printing matters. As in the example below, the later `vcase`s take priority +-- | when parsing/printing: +-- | +-- | ```purescript +-- | userRoutes = +-- | variant +-- | # vcase (Proxy :: _ "list") (pure unit) +-- | # vcase (Proxy :: _ "edit") (string segment) +-- | # vcase (Proxy :: _ "new") (path "new" $ pure unit) +-- | ``` +-- | or +-- | +-- | ```purescript +-- | userRoutes = +-- | variant +-- | # (Proxy :: _ "list") %= (pure unit) +-- | # (Proxy :: _ "edit") %= (string segment) +-- | # (Proxy :: _ "new") %= (path "new" $ pure unit) +-- | ``` +variant :: forall r. RouteDuplex r (Variant ()) +variant = RouteDuplex mempty (Chomp \_ -> Fail EndOfPath) + +-- | Parse/print a single case of a variant. Must be used with `variant`. +vcase + :: forall sym a b r1 r1_ r2 r3 + . IsSymbol sym + => Row.Cons sym a r1_ r1 + => Row.Cons sym b r2 r3 + => Row.Lacks sym r2 + => Proxy sym + -> RouteDuplex a b + -> RouteDuplex (Variant r1_) (Variant r2) + -> RouteDuplex (Variant r1) (Variant r3) +vcase sym (RouteDuplex enc_a dec_b) (RouteDuplex enc_r1 dec_r2) = + RouteDuplex (Variant.on sym enc_a enc_r1) (Variant.inj sym <$> dec_b <|> expand1 sym <$> dec_r2) + where + -- A variant of `Data.Variant.expand` is used in order to avoid adding a + -- redundant `Row.Union` constraint to `vcase`. + expand1 + :: forall sym' lt x gt + . Row.Cons sym' x lt gt + => Proxy sym' + -> Variant lt + -> Variant gt + expand1 _ = unsafeCoerce + +infix 2 vcase as %= + +-- | Match a variant with a record of route duplexes, one for each case. +-- | +-- | We append `rN_` to key names to enforce correct ordering. (To learn more read ["About ordering" section in README](#about-ordering)) +-- | +-- | ```purescript +-- | vmatch +-- | { r1_new: path "new" $ pure unit +-- | , r2_edit: string segment +-- | , r3_list: pure unit +-- | } +-- | ``` +vmatch + :: forall r rl rx + . RowToList r rl + => RouteDuplexVariantMatchCases rl r rx + => Record r + -> RouteDuplex' (Variant rx) +vmatch = vmatchCases (Proxy :: Proxy rl) + +class RouteDuplexVariantMatchCases (rl :: RowList Type) (r :: Row Type) (rx :: Row Type) | rl -> r rx where + vmatchCases :: Proxy rl -> Record r -> RouteDuplex' (Variant rx) + +instance matchCasesNil :: RouteDuplexVariantMatchCases Nil r () where + vmatchCases _ _ = variant + +instance matchCasesCons :: + ( IsSymbol sym + , Row.Cons sym (RouteDuplex' a) r' r + , Row.Cons sym a rx' rx + , Row.Lacks sym rx' + , RouteDuplexVariantMatchCases rest r rx' + ) => + RouteDuplexVariantMatchCases (Cons sym (RouteDuplex' a) rest) r rx where + vmatchCases _ r = + vcase + (Proxy :: Proxy sym) + (Record.get (Proxy :: Proxy sym) r :: RouteDuplex' a) + (vmatchCases (Proxy :: Proxy rest) r) + class RouteDuplexParams (r1 :: Row Type) (r2 :: Row Type) | r1 -> r2 where -- | Builds a `RouteDuplex` from a record of query parameter parsers/printers, where -- | each property corresponds to a query parameter with the same name. diff --git a/test/Main.purs b/test/Main.purs index 0c9fccb..b1eb6e3 100644 --- a/test/Main.purs +++ b/test/Main.purs @@ -2,84 +2,17 @@ module Test.Main where import Prelude hiding ((/)) -import Data.Either (Either(..)) -import Data.Generic.Rep (class Generic) -import Data.Show.Generic (genericShow) -import Data.String.Gen (genAlphaString) import Effect (Effect) -import Routing.Duplex (RouteDuplex', flag, int, param, parse, print, record, rest, root, segment, string, (:=)) -import Routing.Duplex.Generic (noArgs) -import Routing.Duplex.Generic as RDG -import Routing.Duplex.Generic.Syntax ((/), (?)) -import Test.QuickCheck (Result(..), arbitrary, quickCheckGen, (===)) -import Test.QuickCheck.Gen (Gen, arrayOf, chooseInt) -import Test.Unit (combinatorUnitTests) -import Type.Proxy (Proxy(..)) - -data TestRoute - = Root - | Foo String Int String { a :: String, b :: Boolean } - | Bar { id :: String, search :: String } - | Baz String (Array String) - -derive instance eqTestRoute :: Eq TestRoute -derive instance genericTestRoute :: Generic TestRoute _ - -instance showTestRoute :: Show TestRoute where - show = genericShow - -genTestRoute :: Gen TestRoute -genTestRoute = do - chooseInt 1 4 >>= case _ of - 1 -> pure Root - 2 -> - Foo - <$> genAlphaString - <*> arbitrary - <*> genAlphaString - <*> ({ a: _, b: _ } <$> genAlphaString <*> arbitrary) - 3 -> Bar <$> ({ id: _, search: _ } <$> genAlphaString <*> genAlphaString) - _ -> Baz <$> genAlphaString <*> (arrayOf genAlphaString) - -_id = Proxy :: Proxy "id" -_search = Proxy :: Proxy "search" - -route :: RouteDuplex' TestRoute -route = - root $ RDG.sum - { "Root": noArgs - , "Foo": fooRoute - , "Bar": barRoute - , "Baz": bazRoute - } - where - fooRoute = - segment / int segment / segment ? { a: string, b: flag } - - barRoute = - record - # _id := segment - # _search := param "search" - - bazRoute = - segment / rest +import Test.QuickCheck.SumRouteTests as Test.QuickCheck.SumRouteTests +import Test.QuickCheck.VariantRouteTests as Test.QuickCheck.VariantRouteTests +import Test.Spec.Reporter (consoleReporter) +import Test.Spec.Runner.Node (runSpecAndExitProcess) +import Test.Unit.CombinatorTests as Test.Unit.CombinatorTests +import Test.Unit.SumOrderingTests as Test.Unit.SumOrderingTests main :: Effect Unit -main = do - combinatorUnitTests - - quickCheckGen do - r <- genTestRoute - let - url = print route r - res = parse route url - pure $ case res of - Left err -> - Failed $ - show err <> ":" - <> "\n " - <> show r - <> "\n " - <> show url - Right r' -> - r === r' +main = runSpecAndExitProcess [ consoleReporter ] do + Test.Unit.CombinatorTests.tests + Test.Unit.SumOrderingTests.tests + Test.QuickCheck.VariantRouteTests.tests + Test.QuickCheck.SumRouteTests.tests diff --git a/test/QuickCheck/SumRouteTests.purs b/test/QuickCheck/SumRouteTests.purs new file mode 100644 index 0000000..47a6cf9 --- /dev/null +++ b/test/QuickCheck/SumRouteTests.purs @@ -0,0 +1,69 @@ +module Test.QuickCheck.SumRouteTests where + +import Prelude hiding ((/)) + +import Data.Generic.Rep (class Generic) +import Data.Show.Generic (genericShow) +import Data.String.Gen (genAlphaString) +import Routing.Duplex (RouteDuplex', flag, int, param, record, rest, root, segment, string, (:=)) +import Routing.Duplex.Generic (noArgs) +import Routing.Duplex.Generic as RDG +import Routing.Duplex.Generic.Syntax ((/), (?)) +import Test.QuickCheck (arbitrary) +import Test.QuickCheck.Gen (Gen, arrayOf, chooseInt) +import Test.QuickCheck.Utils (printThenParseShouldEqualToInput) +import Test.Spec (Spec, it) +import Test.Spec.QuickCheck (quickCheck) +import Type.Proxy (Proxy(..)) + +data TestRoute + = Foo String Int String { a :: String, b :: Boolean } -- Matches /smth/1/smth?a=smth&b=smth + | Bar { id :: String, search :: String } -- Matches /smth/search + | Baz String (Array String) -- Matches /smth/smth/smth. Baz should be last, because order of parsing is defined by order of constructors. To learn more read ["About ordering" section in README](#about-ordering) + | Root -- Matches / + +derive instance eqTestRoute :: Eq TestRoute +derive instance genericTestRoute :: Generic TestRoute _ + +instance showTestRoute :: Show TestRoute where + show = genericShow + +genTestRoute :: Gen TestRoute +genTestRoute = do + chooseInt 1 4 >>= case _ of + 1 -> pure Root + 2 -> + Foo + <$> genAlphaString + <*> arbitrary + <*> genAlphaString + <*> ({ a: _, b: _ } <$> genAlphaString <*> arbitrary) + 3 -> Bar <$> ({ id: _, search: _ } <$> genAlphaString <*> genAlphaString) + _ -> Baz <$> genAlphaString <*> (arrayOf genAlphaString) + +_id = Proxy :: Proxy "id" +_search = Proxy :: Proxy "search" + +route :: RouteDuplex' TestRoute +route = + root $ RDG.sum + { "Baz": bazRoute + , "Bar": barRoute + , "Foo": fooRoute + , "Root": noArgs + } + where + fooRoute = + segment / int segment / segment ? { a: string, b: flag } + + barRoute = + record + # _id := segment + # _search := param "search" + + bazRoute = + segment / rest + +tests :: Spec Unit +tests = it "NormalRouteTests" do + quickCheck $ genTestRoute >>= printThenParseShouldEqualToInput route diff --git a/test/QuickCheck/Utils.purs b/test/QuickCheck/Utils.purs new file mode 100644 index 0000000..a26e703 --- /dev/null +++ b/test/QuickCheck/Utils.purs @@ -0,0 +1,32 @@ +module Test.QuickCheck.Utils where + +import Prelude hiding ((/)) + +import Data.Either (Either(..)) +import Routing.Duplex (RouteDuplex', parse, print) +import Test.QuickCheck (Result(..)) +import Test.QuickCheck.Gen (Gen) + +printThenParseShouldEqualToInput :: forall i. Show i => Eq i => RouteDuplex' i -> i -> Gen Result +printThenParseShouldEqualToInput routeDuplex inputRoute = do + let inputRoute_url = print routeDuplex inputRoute + let parsedTestRoute_result = parse routeDuplex inputRoute_url + pure $ case parsedTestRoute_result of + Left parsedTestRoute_result_error -> + Failed $ + show parsedTestRoute_result_error <> ":" + <> "\n " + <> show inputRoute + <> "\n URL: " + <> show inputRoute_url + Right parsedTestRoute -> + if inputRoute == parsedTestRoute then Success + else Failed $ + let + parsedTestRoute_url = print routeDuplex parsedTestRoute + in + "Input route " <> show inputRoute <> " !== parsed route " <> show parsedTestRoute <> ":" + <> "\n Input URL: " + <> show inputRoute_url + <> "\n Parsed URL: " + <> show parsedTestRoute_url diff --git a/test/QuickCheck/VariantRouteTests.purs b/test/QuickCheck/VariantRouteTests.purs new file mode 100644 index 0000000..ce1b8ad --- /dev/null +++ b/test/QuickCheck/VariantRouteTests.purs @@ -0,0 +1,64 @@ +module Test.QuickCheck.VariantRouteTests where + +import Prelude + +import Data.Either (Either(..)) +import Data.String.Gen (genAlphaString) +import Data.Variant as V +import Data.Variant.Gen (genVariantUniform) +import Routing.Duplex (RouteDuplex', end, parse, path, print, segment, string, variant, vmatch, (%=)) +import Test.QuickCheck.Utils (printThenParseShouldEqualToInput) +import Test.Spec (Spec, describe, it) +import Test.Spec.Assertions (shouldEqual) +import Test.Spec.QuickCheck (quickCheck) +import Type.Proxy (Proxy(..)) + +_x = Proxy :: Proxy "x" +_y = Proxy :: Proxy "y" + +tests :: Spec Unit +tests = describe "VariantTests" do + it "quickCheck variant" $ quickCheck $ genVariantUniform { x: genAlphaString, y: pure unit } >>= printThenParseShouldEqualToInput ((_x %= segment) ((_y %= (pure unit)) variant)) + it "quickCheck vmatch" $ quickCheck $ genVariantUniform { x: genAlphaString, y: pure unit } >>= printThenParseShouldEqualToInput (vmatch { x: segment, y: pure unit :: RouteDuplex' Unit }) + it "quickCheck variant -> realworld" do + let + -- order of parsing is new -> edit -> list, bc is defined by order of function execution. To learn more read ["About ordering" section in README](#about-ordering) + routeDuplex = variant + # (Proxy :: _ "list") %= (pure unit) + # (Proxy :: _ "edit") %= (string segment) + # (Proxy :: _ "new") %= (path "new" $ pure unit) + generator = { new: pure unit, edit: genAlphaString, list: pure unit } + quickCheck $ genVariantUniform generator >>= printThenParseShouldEqualToInput routeDuplex + it "quickCheck vmatch -> realworld" do + let + routeDuplex = vmatch + { r1_new: path "new" $ pure unit :: RouteDuplex' Unit + , r2_edit: string segment + , r3_list: pure unit :: RouteDuplex' Unit -- r3_list should have such a name to be parsed last, bc order of parsing is defined by names of keys. To learn more read ["About ordering" section in README](#about-ordering) + } + generator = { r1_new: pure unit, r2_edit: genAlphaString, r3_list: pure unit } + quickCheck $ genVariantUniform generator >>= printThenParseShouldEqualToInput routeDuplex + it "Parsing and printing using variant and vmatch" do + parse ((_y %= segment) ((_x %= (pure unit)) variant)) "a/b" `shouldEqual` Right (V.inj _y "a") + parse (vmatch { y: segment, x: pure unit :: RouteDuplex' Unit }) "a/b" `shouldEqual` Right (V.inj _x unit) -- not same output, ordering doesnt matter, only name of keys + + print ((_y %= segment) ((_x %= (pure unit)) variant)) (V.inj _y "a/b") `shouldEqual` "a%2Fb" + print (vmatch { y: segment, x: pure unit :: RouteDuplex' Unit }) (V.inj _y "a/b") `shouldEqual` "a%2Fb" + ------ + parse ((_x %= (pure unit)) ((_y %= segment) variant)) "a/b" `shouldEqual` Right (V.inj _x unit) + parse (vmatch { x: pure unit :: RouteDuplex' Unit, y: segment }) "a/b" `shouldEqual` Right (V.inj _x unit) + -- + print ((_x %= (pure unit)) ((_y %= segment) variant)) (V.inj _x unit) `shouldEqual` "" + print (vmatch { x: pure unit :: RouteDuplex' Unit, y: segment }) (V.inj _x unit) `shouldEqual` "" + --- + parse ((_y %= segment) ((_x %= (end $ pure unit)) variant)) "a/b" `shouldEqual` Right (V.inj _y "a") + parse (vmatch { y: segment, x: end $ pure unit :: RouteDuplex' Unit }) "a/b" `shouldEqual` Right (V.inj _y "a") + + print ((_y %= segment) ((_x %= (end $ pure unit)) variant)) (V.inj _y "a/b") `shouldEqual` "a%2Fb" + print (vmatch { y: segment, x: end $ pure unit :: RouteDuplex' Unit }) (V.inj _y "a/b") `shouldEqual` "a%2Fb" + ------ + parse ((_x %= (end $ pure unit)) ((_y %= segment) variant)) "a/b" `shouldEqual` Right (V.inj _y "a") + parse (vmatch { x: end $ pure unit :: RouteDuplex' Unit, y: segment }) "a/b" `shouldEqual` Right (V.inj _y "a") + + print ((_x %= (end $ pure unit)) ((_y %= segment) variant)) (V.inj _x unit) `shouldEqual` "" + print (vmatch { x: end $ pure unit :: RouteDuplex' Unit, y: segment }) (V.inj _x unit) `shouldEqual` "" diff --git a/test/Unit.purs b/test/Unit.purs deleted file mode 100644 index 23b4af8..0000000 --- a/test/Unit.purs +++ /dev/null @@ -1,143 +0,0 @@ -module Test.Unit (combinatorUnitTests) where - -import Prelude - -import Data.Either (Either(..)) -import Data.Maybe (Maybe(..)) -import Effect (Effect) -import Routing.Duplex (RouteDuplex', as, boolean, default, flag, hash, int, many, many1, optional, param, params, parse, path, prefix, print, prop, record, rest, root, segment, string, suffix) -import Routing.Duplex.Parser (RouteError(..), parsePath) -import Test.Assert (assertEqual) -import Type.Proxy (Proxy(..)) - -combinatorUnitTests :: Effect Unit -combinatorUnitTests = do - -- boolean - assertEqual { actual: parse (boolean segment) "true", expected: Right true } - assertEqual { actual: parse (boolean segment) "false", expected: Right false } - assertEqual { actual: parse (boolean segment) "x", expected: Left (Expected "Boolean" "x") } - assertEqual { actual: parse (boolean segment) "", expected: Left EndOfPath } - - -- prefix - assertEqual { actual: parse (prefix "api" segment) "api/a", expected: Right "a" } - assertEqual { actual: parse (prefix "api" segment) "api/a", expected: Right "a" } - assertEqual { actual: parse (prefix "/api/v1" segment) "%2Fapi%2Fv1/a", expected: Right "a" } - assertEqual { actual: parse (prefix "/api/v1" segment) "/api/v1/a", expected: Left (Expected "/api/v1" "") } - - -- path - assertEqual { actual: parse (path "/api/v1" segment) "/api/v1/a", expected: Right "a" } - assertEqual { actual: parse (path "/api/v1" segment) "/api/v2/a", expected: Left (Expected "v1" "v2") } - - -- segment - assertEqual { actual: parse segment "abc", expected: Right "abc" } - assertEqual { actual: parse segment "abc%20def", expected: Right "abc def" } - assertEqual { actual: parse segment "abc/def", expected: Right "abc" } - assertEqual { actual: parse segment "/abc", expected: Right "" } - assertEqual { actual: parse segment "", expected: Left EndOfPath } - - -- root - assertEqual { actual: parse (root segment) "/abc", expected: Right "abc" } - assertEqual { actual: parse (root segment) "abc", expected: Left (Expected "" "abc") } - assertEqual { actual: parse (root segment) "/", expected: Left EndOfPath } - - -- int - assertEqual { actual: parse (int segment) "1", expected: Right 1 } - assertEqual { actual: parse (int segment) "x", expected: Left (Expected "Int" "x") } - - -- param - assertEqual { actual: parse (param "search") "?search=keyword", expected: Right "keyword" } - assertEqual { actual: parse (param "search") "/", expected: Left (MissingParam "search") } - assertEqual { actual: parse (optional (param "search")) "/", expected: Right Nothing } - - -- hash - assertEqual { actual: parse hash "abc#def", expected: Right "def" } - - -- suffix - assertEqual { actual: parse (suffix segment "latest") "release/latest", expected: Right "release" } - assertEqual { actual: parse (suffix segment "latest") "/latest", expected: Right "" } - assertEqual { actual: parse (suffix segment "x/y") "a/x%2Fy", expected: Right "a" } - assertEqual { actual: parse (suffix segment "latest") "/", expected: Left EndOfPath } - assertEqual { actual: parse (suffix segment "x/y") "a/x/y", expected: Left (Expected "x/y" "x") } - - -- rest - assertEqual { actual: parse rest "", expected: Right [] } - assertEqual { actual: parse rest "a/b", expected: Right [ "a", "b" ] } - assertEqual { actual: parse (path "a/b" rest) "a/b/c/d", expected: Right [ "c", "d" ] } - assertEqual { actual: print rest [ "a", "b" ], expected: "a/b" } - - -- default - assertEqual { actual: parse (default 0 $ int segment) "1", expected: Right 1 } - assertEqual { actual: parse (default 0 $ int segment) "x", expected: Right 0 } - - -- as - assertEqual { actual: parse (sort segment) "asc", expected: Right Asc } - assertEqual { actual: parse (sort segment) "x", expected: Left (Expected "asc or desc" "x") } - - -- many1 - assertEqual { actual: parse (many1 (int segment)) "1/2/3/x", expected: Right [ 1, 2, 3 ] } - assertEqual { actual: parse (many1 (int segment)) "x", expected: Left (Expected "Int" "x") :: Either RouteError (Array Int) } - - -- many - assertEqual { actual: parse (many (int segment)) "1/2/3/x", expected: Right [ 1, 2, 3 ] } - assertEqual { actual: parse (many (int segment)) "x", expected: Right [] } - - -- flag - assertEqual { actual: parse (flag (param "x")) "?x", expected: Right true } - assertEqual { actual: parse (flag (param "x")) "?x=true", expected: Right true } - assertEqual { actual: parse (flag (param "x")) "?x=false", expected: Right true } - assertEqual { actual: parse (flag (param "x")) "?y", expected: Right false } - - -- string - assertEqual { actual: parse (string segment) "x", expected: Right "x" } - assertEqual { actual: parse (string segment) "%20", expected: Right " " } - - -- optional - assertEqual { actual: parse (optional segment) "a", expected: Right (Just "a") } - assertEqual { actual: parse (optional segment) "", expected: Right Nothing } - assertEqual { actual: print (optional segment) (Just "a"), expected: "a" } - assertEqual { actual: print (optional segment) Nothing, expected: "" } - - -- record - assertEqual { actual: parse (path "blog" date) "blog/2019/1/2", expected: Right { year: 2019, month: 1, day: 2 } } - - -- params - assertEqual { actual: parse search "?page=3&filter=Galaxy%20Quest", expected: Right { page: 3, filter: Just "Galaxy Quest" } } - - -- Malformed URI component - assertEqual { actual: parsePath "https://example.com?keyword=%D0%BF%D0", expected: Left (MalformedURIComponent "%D0%BF%D0") } - assertEqual { actual: print (path "foo" segment) "\xdc11", expected: "foo" } - -data Sort = Asc | Desc - -derive instance eqSort :: Eq Sort -instance showSort :: Show Sort where - show Asc = "asc" - show Desc = "desc" - -sortToString :: Sort -> String -sortToString = case _ of - Asc -> "asc" - Desc -> "desc" - -sortFromString :: String -> Either String Sort -sortFromString = case _ of - "asc" -> Right Asc - "desc" -> Right Desc - _ -> Left $ "asc or desc" - -sort :: RouteDuplex' String -> RouteDuplex' Sort -sort = as sortToString sortFromString - -date :: RouteDuplex' { year :: Int, month :: Int, day :: Int } -date = - record - # prop (Proxy :: _ "year") (int segment) - # prop (Proxy :: _ "month") (int segment) - # prop (Proxy :: _ "day") (int segment) - -search :: RouteDuplex' { page :: Int, filter :: Maybe String } -search = - params - { page: int - , filter: optional <<< string - } diff --git a/test/Unit/CombinatorTests.purs b/test/Unit/CombinatorTests.purs new file mode 100644 index 0000000..a8faae4 --- /dev/null +++ b/test/Unit/CombinatorTests.purs @@ -0,0 +1,161 @@ +module Test.Unit.CombinatorTests (tests) where + +import Prelude + +import Data.Either (Either(..)) +import Data.Maybe (Maybe(..)) +import Routing.Duplex (RouteDuplex', as, boolean, default, end, flag, hash, int, many, many1, optional, param, params, parse, path, prefix, print, prop, record, rest, root, segment, string, suffix) +import Routing.Duplex.Parser (RouteError(..), parsePath) +import Test.Spec (Spec, it) +import Test.Spec.Assertions (shouldEqual) +import Type.Proxy (Proxy(..)) + +tests :: Spec Unit +tests = it "CombinatorTests" do + -- boolean + parse (boolean segment) "true" `shouldEqual` Right true + parse (boolean segment) "false" `shouldEqual` Right false + parse (boolean segment) "x" `shouldEqual` Left (Expected "Boolean" "x") + parse (boolean segment) "" `shouldEqual` Left EndOfPath + + -- prefix + parse (prefix "api" segment) "api/a" `shouldEqual` Right "a" + parse (prefix "api" segment) "api/a" `shouldEqual` Right "a" + parse (prefix "/api/v1" segment) "%2Fapi%2Fv1/a" `shouldEqual` Right "a" + parse (prefix "/api/v1" segment) "/api/v1/a" `shouldEqual` Left (Expected "/api/v1" "") + + -- path + parse (path "/api/v1" segment) "/api/v1/a" `shouldEqual` Right "a" + parse (path "/api/v1" segment) "/api/v2/a" `shouldEqual` Left (Expected "v1" "v2") + + -- segment + parse segment "abc" `shouldEqual` Right "abc" + parse segment "abc%20def" `shouldEqual` Right "abc def" + parse segment "abc/def" `shouldEqual` Right "abc" + parse segment "/abc" `shouldEqual` Right "" + parse segment "" `shouldEqual` Left EndOfPath + + -- root + parse (root segment) "/abc" `shouldEqual` Right "abc" + parse (root segment) "abc" `shouldEqual` Left (Expected "" "abc") + parse (root segment) "/" `shouldEqual` Left EndOfPath + + -- int + parse (int segment) "1" `shouldEqual` Right 1 + parse (int segment) "x" `shouldEqual` Left (Expected "Int" "x") + + -- param + parse (param "search") "?search=keyword" `shouldEqual` Right "keyword" + parse (param "search") "/" `shouldEqual` Left (MissingParam "search") + parse (optional (param "search")) "/" `shouldEqual` Right Nothing + + -- hash + parse hash "abc#def" `shouldEqual` Right "def" + + -- suffix + parse (suffix segment "latest") "release/latest" `shouldEqual` Right "release" + parse (suffix segment "latest") "/latest" `shouldEqual` Right "" + parse (suffix segment "x/y") "a/x%2Fy" `shouldEqual` Right "a" + parse (suffix segment "latest") "/" `shouldEqual` Left EndOfPath + parse (suffix segment "x/y") "a/x/y" `shouldEqual` Left (Expected "x/y" "x") + + -- rest + parse rest "" `shouldEqual` Right [] + parse rest "a/b" `shouldEqual` Right [ "a", "b" ] + parse (path "a/b" rest) "a/b/c/d" `shouldEqual` Right [ "c", "d" ] + print rest [ "a", "b" ] `shouldEqual` "a/b" + + -- default + parse (default 0 $ int segment) "1" `shouldEqual` Right 1 + parse (default 0 $ int segment) "x" `shouldEqual` Right 0 + + -- as + parse (sort segment) "asc" `shouldEqual` Right Asc + parse (sort segment) "x" `shouldEqual` Left (Expected "asc or desc" "x") + + -- many1 + parse (many1 (int segment)) "1/2/3/x" `shouldEqual` Right [ 1, 2, 3 ] + parse (many1 (int segment)) "x" `shouldEqual` (Left (Expected "Int" "x") :: Either RouteError (Array Int)) + + -- many + parse (many (int segment)) "1/2/3/x" `shouldEqual` Right [ 1, 2, 3 ] + parse (many (int segment)) "x" `shouldEqual` Right [] + + -- flag + parse (flag (param "x")) "?x" `shouldEqual` Right true + parse (flag (param "x")) "?x=true" `shouldEqual` Right true + parse (flag (param "x")) "?x=false" `shouldEqual` Right true + parse (flag (param "x")) "?y" `shouldEqual` Right false + + -- string + parse (string segment) "x" `shouldEqual` Right "x" + parse (string segment) "%20" `shouldEqual` Right " " + + -- optional + parse (optional segment) "a" `shouldEqual` Right (Just "a") + parse (optional segment) "" `shouldEqual` Right Nothing + print (optional segment) (Just "a") `shouldEqual` "a" + print (optional segment) Nothing `shouldEqual` "" + + -- record + parse (path "blog" date) "blog/2019/1/2" `shouldEqual` Right { year: 2019, month: 1, day: 2 } + + -- params + parse search "?page=3&filter=Galaxy%20Quest" `shouldEqual` Right { page: 3, filter: Just "Galaxy Quest" } + + -- Malformed URI component + parsePath "https://example.com?keyword=%D0%BF%D0" `shouldEqual` Left (MalformedURIComponent "%D0%BF%D0") + print (path "foo" segment) "\xdc11" `shouldEqual` "foo" + + -- `pure unit` matches anything + parse (pure unit) "" `shouldEqual` Right unit + parse (pure unit) "a" `shouldEqual` Right unit + parse (pure unit) "/asdf" `shouldEqual` Right unit + -- `pure unit` prints to "" + print (pure unit) (Just "asdf/asdf") `shouldEqual` "" + print (pure unit) Nothing `shouldEqual` "" + + -- `end $ pure unit` matches only "" + parse (end $ pure unit) "" `shouldEqual` Right unit + parse (end $ pure unit) "a" `shouldEqual` Left (ExpectedEndOfPath "a") + parse (end $ pure unit) "a/" `shouldEqual` Left (ExpectedEndOfPath "a") + parse (end $ pure unit) "a/b" `shouldEqual` Left (ExpectedEndOfPath "a") + parse (end $ pure unit) "/asdf" `shouldEqual` Left (ExpectedEndOfPath "") + -- `end $ pure unit` prints to "" + print (end $ pure unit) (Just "asdf/asdf") `shouldEqual` "" + print (end $ pure unit) Nothing `shouldEqual` "" + +data Sort = Asc | Desc + +derive instance eqSort :: Eq Sort +instance showSort :: Show Sort where + show Asc = "asc" + show Desc = "desc" + +sortToString :: Sort -> String +sortToString = case _ of + Asc -> "asc" + Desc -> "desc" + +sortFromString :: String -> Either String Sort +sortFromString = case _ of + "asc" -> Right Asc + "desc" -> Right Desc + _ -> Left $ "asc or desc" + +sort :: RouteDuplex' String -> RouteDuplex' Sort +sort = as sortToString sortFromString + +date :: RouteDuplex' { year :: Int, month :: Int, day :: Int } +date = + record + # prop (Proxy :: _ "year") (int segment) + # prop (Proxy :: _ "month") (int segment) + # prop (Proxy :: _ "day") (int segment) + +search :: RouteDuplex' { page :: Int, filter :: Maybe String } +search = + params + { page: int + , filter: optional <<< string + } diff --git a/test/Unit/SumOrderingTests.purs b/test/Unit/SumOrderingTests.purs new file mode 100644 index 0000000..06c3968 --- /dev/null +++ b/test/Unit/SumOrderingTests.purs @@ -0,0 +1,38 @@ +module Test.Unit.SumOrderingTests (tests) where + +import Prelude + +import Data.Either (Either(..)) +import Data.Generic.Rep (class Generic) +import Data.Show.Generic (genericShow) +import Routing.Duplex (parse, root, segment) +import Routing.Duplex.Generic as RDG +import Test.Spec (Spec, it) +import Test.Spec.Assertions (shouldEqual) + +-- 'sum' parsing depends on position of constructor. To learn more read ["About ordering" section in README](#about-ordering) +tests :: Spec Unit +tests = it "sum - order of parsing is defined by ordering of constructors in definintion of datatype" do + -- they both parse /foo, but one is more segment that the other + parse (root $ RDG.sum { "AB1_A": segment, "AB1_B": segment }) "/1" `shouldEqual` Right (AB1_A "1") + parse (root $ RDG.sum { "AB1_B": segment, "AB1_A": segment }) "/1" `shouldEqual` Right (AB1_A "1") + + -- now - mirror kingdom + parse (root $ RDG.sum { "AB2_A": segment, "AB2_B": segment }) "/1" `shouldEqual` Right (AB2_B "1") + parse (root $ RDG.sum { "AB2_B": segment, "AB2_A": segment }) "/1" `shouldEqual` Right (AB2_B "1") + +-------------------- +data AB1 = AB1_A String | AB1_B String + +derive instance Eq (AB1) +derive instance Generic (AB1) _ +instance Show (AB1) where + show = genericShow + +----------- same like previous but constructors order definition is reversed +data AB2 = AB2_B String | AB2_A String + +derive instance Eq (AB2) +derive instance Generic (AB2) _ +instance Show (AB2) where + show = genericShow