├── .github └── workflows │ ├── release.yml │ └── tests.yml ├── .ignore ├── .typstignore ├── CHANGELOG.md ├── Justfile ├── LICENSE ├── README.md ├── docs ├── manual.pdf └── manual.typ ├── scripts ├── package ├── setup └── uninstall ├── src ├── assertions-util.typ ├── assertions.typ ├── assertions │ ├── comparative.typ │ ├── length.typ │ └── string.typ ├── base-type.typ ├── coercions.typ ├── ctx.typ ├── lib.typ ├── schemas.typ ├── schemas │ ├── author.typ │ └── enumerations.typ ├── types.typ └── types │ ├── array.typ │ ├── dictionary.typ │ ├── logical.typ │ ├── number.typ │ ├── sink.typ │ ├── string.typ │ └── tuple.typ ├── tests ├── .gitignore ├── .ignore ├── assertions │ └── comparative │ │ ├── ref │ │ └── 1.png │ │ └── test.typ ├── contexts │ ├── remove-optional-none │ │ ├── ref │ │ │ └── 1.png │ │ └── test.typ │ └── strict │ │ ├── ref │ │ └── 1.png │ │ └── test.typ ├── logical │ ├── ref │ │ └── 1.png │ └── test.typ ├── schemas │ └── author │ │ ├── ref │ │ └── 1.png │ │ └── test.typ ├── types │ ├── any │ │ ├── ref │ │ │ └── 1.png │ │ └── test.typ │ ├── array │ │ ├── ref │ │ │ └── 1.png │ │ └── test.typ │ ├── boolean │ │ ├── ref │ │ │ └── 1.png │ │ └── test.typ │ ├── choice │ │ ├── ref │ │ │ └── 1.png │ │ └── test.typ │ ├── color │ │ ├── ref │ │ │ └── 1.png │ │ └── test.typ │ ├── content │ │ ├── ref │ │ │ └── 1.png │ │ └── test.typ │ ├── datetime │ │ ├── ref │ │ │ └── 1.png │ │ └── test.typ │ ├── dictionary │ │ ├── ref │ │ │ └── 1.png │ │ └── test.typ │ ├── gradient │ │ ├── ref │ │ │ └── 1.png │ │ └── test.typ │ ├── number │ │ ├── ref │ │ │ └── 1.png │ │ └── test.typ │ ├── sink │ │ ├── ref │ │ │ └── 1.png │ │ └── test.typ │ ├── string │ │ ├── ref │ │ │ └── 1.png │ │ └── test.typ │ ├── stroke │ │ ├── ref │ │ │ └── 1.png │ │ └── test.typ │ ├── tuple │ │ ├── ref │ │ │ ├── 1.png │ │ │ └── 2.png │ │ └── test.typ │ └── version │ │ ├── ref │ │ └── 1.png │ │ └── test.typ └── utility.typ └── typst.toml /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Package and push to registry repo 2 | on: 3 | push: 4 | tags: [ v* ] 5 | 6 | env: 7 | # the repository to which to push the release version 8 | # usually a fork of typst/packages (https://github.com/typst/packages/) 9 | # that you have push privileges to 10 | REGISTRY_REPO: typst-community/packages 11 | # the path within that repo where the "/" directory should be put 12 | # for the Typst package registry, keep this as is 13 | PATH_PREFIX: packages/preview 14 | 15 | jobs: 16 | release: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | 22 | - name: Probe runner package cache 23 | uses: awalsh128/cache-apt-pkgs-action@v1 24 | with: 25 | packages: cargo 26 | version: 1.0 27 | 28 | - name: Install just from crates.io 29 | uses: baptiste0928/cargo-install@v3 30 | with: 31 | crate: just 32 | 33 | - name: Setup typst 34 | uses: typst-community/setup-typst@v3 35 | with: 36 | typst-version: latest 37 | 38 | - name: Determine and check package metadata 39 | run: | 40 | . scripts/setup 41 | echo "PKG_NAME=${PKG_PREFIX}" >> "${GITHUB_ENV}" 42 | echo "PKG_VERSION=${VERSION}" >> "${GITHUB_ENV}" 43 | 44 | if [[ "${GITHUB_REF_NAME}" != "v${VERSION}" ]]; then 45 | echo "package version ${VERSION} does not match release tag ${GITHUB_REF_NAME}" >&2 46 | exit 1 47 | fi 48 | 49 | - name: Build package 50 | run: | 51 | just doc 52 | just package out 53 | 54 | - name: Checkout package registry 55 | uses: actions/checkout@v4 56 | with: 57 | repository: ${{ env.REGISTRY_REPO }} 58 | token: ${{ secrets.REGISTRY_TOKEN }} 59 | path: typst-packages 60 | 61 | - name: Release package 62 | run: | 63 | mkdir -p "typst-packages/${{ env.PATH_PREFIX }}/$PKG_NAME" 64 | mv "out/${PKG_NAME}/${PKG_VERSION}" "typst-packages/${{ env.PATH_PREFIX }}/${PKG_NAME}" 65 | rmdir "out/${PKG_NAME}" 66 | rmdir out 67 | 68 | GIT_USER_NAME="$(git log -1 --pretty=format:'%an')" 69 | GIT_USER_EMAIL="$(git log -1 --pretty=format:'%ae')" 70 | 71 | cd typst-packages 72 | git config user.name "${GIT_USER_NAME}" 73 | git config user.email "${GIT_USER_EMAIL}" 74 | git checkout -b "${PKG_NAME}-${PKG_VERSION}" 75 | git remote remove origin 76 | git remote add origin https://${{ secrets.REGISTRY_TOKEN }}]@github.com/${{ env.REGISTRY_REPO }} 77 | git add . 78 | git commit -m "${PKG_NAME}:${PKG_VERSION}" 79 | git push --set-upstream origin "${PKG_NAME}-${PKG_VERSION}" 80 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | push: 4 | branches: [ main ] 5 | pull_request: 6 | branches: [ main ] 7 | 8 | jobs: 9 | tests: 10 | strategy: 11 | matrix: 12 | # add any other Typst versions that your package should support 13 | typst-version: ["0.13.0-rc1"] 14 | # the docs don't need to build with all versions supported by the package; 15 | # the latest one is enough 16 | include: 17 | - typst-version: "0.13.0-rc1" 18 | doc: 1 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | 24 | - name: Probe runner package cache 25 | uses: awalsh128/cache-apt-pkgs-action@v1 26 | with: 27 | packages: imagemagick cargo 28 | version: 1.0 29 | 30 | - name: Install oxipng from crates.io 31 | uses: baptiste0928/cargo-install@v3 32 | with: 33 | crate: oxipng 34 | 35 | - name: Install just from crates.io 36 | uses: baptiste0928/cargo-install@v3 37 | with: 38 | crate: just 39 | 40 | - name: Install typst-test from github 41 | uses: baptiste0928/cargo-install@v3 42 | with: 43 | crate: typst-test 44 | git: https://github.com/tingerrr/typst-test.git 45 | tag: ci-semi-stable 46 | 47 | - name: Setup typst 48 | uses: typst-community/setup-typst@v3 49 | with: 50 | typst-version: ${{ matrix.typst-version }} 51 | 52 | - name: Run test suite 53 | run: just test 54 | 55 | - name: Build docs 56 | if: ${{ matrix.doc }} 57 | run: just doc 58 | -------------------------------------------------------------------------------- /.ignore: -------------------------------------------------------------------------------- 1 | **.png 2 | **.jpg 3 | **.pdf 4 | -------------------------------------------------------------------------------- /.typstignore: -------------------------------------------------------------------------------- 1 | # this is not a "standard" ignore file, it's specific to this template's `scripts/package` script 2 | # list any files here that should not be uploaded to Universe when releasing this package 3 | 4 | # if you are used to ignore files, be aware that .typstignore is a bit more limited: 5 | # - only this file is used; .typstignore files in subdirectories are not considered 6 | # - patterns must match file/directory names from the beginning: `x.typ` will not match `src/x.typ` 7 | # - `*` in patterns works, but also matches directory separators: `*.typ` _will_ match `src/x.typ` 8 | # .git and .typstignore are excluded automatically 9 | 10 | .github 11 | scripts 12 | tests 13 | Justfile 14 | # PDF manuals should be included so that they can be linked, but not their sources 15 | docs/* 16 | !docs/*.pdf 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [v0.2.2](https://github.com/typst-community/valakyrie/releases/tags/v0.2.1) 2 | 3 | ## Added 4 | 5 | - Added negated equality assertions for comparative and length. (#40) 6 | 7 | ## Changed 8 | 9 | ## Fixed 10 | - Panic related to deprecated type check behavior in 0.13.0 11 | 12 | # [v0.2.1](https://github.com/typst-community/valakyrie/releases/tags/v0.2.1) 13 | 14 | 15 | ## Added 16 | - Added schema generators for: angle, bytes, direction, fraction, function, label, length, location, plugin, ratio, relative, regex, selector, stroke, symbol, and version 17 | 18 | ## Changed 19 | - Valkyrie is now distributed under the MIT license rather than GPL-3.0-only. 20 | - Number schema generator now takes additional optional parameters `min` and `max` which a sugar for value assertions. These changes also apply to number specializations such as `float` and `integer` 21 | - String schema generator now takes additional optional parameters `min` and `max` which a sugar for value length assertions. These changes also apply to number specializations such as `email` and `ip` 22 | - Array schema generator now takes additional optional parameters `min` and `max` which a sugar for value length assertions. 23 | - Added schema generator for argument sinks. Takes `positional` and `named` as optional parameters that take schema types. Absence of one of these parameters indicate that these must also be absent from the argument type being validated. 24 | - **(Potentially Breaking)** Content now accepts `symbol` as a valid input type by default (see #20) 25 | - **(Potentially Breaking)** If the tested value is `auto`, parsing no longer fails. If `default` is set, it takes the default value. If `optional` is set but not `default`, value is parsed as `none`. 26 | - **(Potentially Breaking)** `tuple` has a new parameter `exact` which defaults to true, whereby the length of a tuple must match exactly to be valid. 27 | 28 | --- 29 | 30 | # [v0.2.0](https://github.com/typst-community/valakyrie/releases/tags/v0.2.0) 31 | 32 | `Valkyrie` is now a community-lead project and is now homed on the typst-community organisation. 33 | 34 | ## Added 35 | - `Boolean` validation type 36 | - `Content` validation type. Also accepts strings which are coerced into content types. 37 | - `Color` validation type. 38 | - `Optional` validation type. If a schema yields a validation error, the error is suppressed and the returned value is 'auto' 39 | - `Choice` validation type. Tested value must be contained within the listed choices. 40 | - `Date` validation type. 41 | - Dictionaries can now provide a list of aliases for members. 42 | - Dictionaries can now be coerced from values 43 | - Arrays can be coerced from singular values. 44 | 45 | ## Removed 46 | 47 | ## Changed 48 | - **(Breaking)** Schema generator function arguments are now uniform. Types are all now effectively a curried form of `base-type`. A table is provided in the manual to document these arguments. 49 | - **(Breaking)** `transform` argument has been replaced with `pre-transform` (applied prior to validation), and `post-transform` (applied after validation). 50 | - **(Breaking)** Dictionaries now take the schema definition of members as a dictionary in the first positional argument rather than a sink of named arguments. 51 | - **(Breaking)** `strict` contextual flag is now applied on the type level rather than directly in the parse function. It is currently only applied in the dictionary type, and will cause an assertion to fail if s 52 | - **(Breaking)** Dictionaries default to empty dictionaries rather than none. 53 | - **(Potentially breaking)** Dictionaries that don't have a member that is present in the schema no longer produce an error outside of `strict` contexts. 54 | - **(Potentially breaking)** `strict` contextual flag is now applied on the type level rather than directly in the parse function. It is currently only applied in the dictionary type, and will cause an assertion to fail if set to `true` when object being validated contains keys unknown to the schema. 55 | - **(Breaking)** Assertions on values have been moved from named arguments to assertion generator functions. 56 | 57 | ## Migration guide from v0.1.X 58 | - Dictionary schema definitions will need additional braces to account for a change in the API layout 59 | - `transform` is now `pre-transform`, and can be used to coerce values into types 60 | - Assertions regarding the value being validated (length, magnitude, regex match) have now been moved from being named arguments to being passed in the `assertions` argument as an array. 61 | 62 | --- 63 | 64 | # [v0.1.1](https://github.com/typst-community/valakyrie/releases/tags/v0.1.1) 65 | ## Changed 66 | - fixed syntax error in Typst 0.11+ because of internal context type 67 | 68 | --- 69 | 70 | # [v0.1.0](https://github.com/typst-community/valakyrie/releases/tags/v0.1.0) 71 | Initial Release 72 | -------------------------------------------------------------------------------- /Justfile: -------------------------------------------------------------------------------- 1 | root := justfile_directory() 2 | 3 | export TYPST_ROOT := root 4 | 5 | [private] 6 | default: 7 | @just --list --unsorted 8 | 9 | # generate manual 10 | doc: 11 | typst compile docs/manual.typ docs/manual.pdf 12 | 13 | # run test suite 14 | test *args: 15 | typst-test run {{ args }} 16 | 17 | # update test cases 18 | update *args: 19 | typst-test update {{ args }} 20 | 21 | # package the library into the specified destination folder 22 | package target: 23 | ./scripts/package "{{target}}" 24 | 25 | # install the library with the "@local" prefix 26 | install: (package "@local") 27 | 28 | # install the library with the "@preview" prefix (for pre-release testing) 29 | install-preview: (package "@preview") 30 | 31 | [private] 32 | remove target: 33 | ./scripts/uninstall "{{target}}" 34 | 35 | # uninstalls the library from the "@local" prefix 36 | uninstall: (remove "@local") 37 | 38 | # uninstalls the library from the "@preview" prefix (for pre-release testing) 39 | uninstall-preview: (remove "@preview") 40 | 41 | # run ci suite 42 | ci: test doc 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # Released under MIT License 2 | 3 | Copyright (c) 2023 James Swift. 4 | 5 | Copyright (c) 2024 Tinger . 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The `Valkyrie` Package 2 |
Version 0.2.2
3 | 4 | This package implements type validation, and is targeted mainly at package and template developers. The desired outcome is that it becomes easier for the programmer to quickly put a package together without spending a long time on type safety, but also to make the usage of those packages by end-users less painful by generating useful error messages. 5 | 6 | ## Example Usage 7 | ```typ 8 | #import "@preview/valkyrie:0.2.2" as z 9 | 10 | #let my-schema = z.dictionary(( 11 | should-be-string: z.string(), 12 | complicated-tuple: z.tuple( 13 | z.email(), 14 | z.ip(), 15 | z.either( 16 | z.string(), 17 | z.number(), 18 | ), 19 | ), 20 | ) 21 | ) 22 | 23 | #z.parse( 24 | ( 25 | should-be-string: "This doesn't error", 26 | complicated-tuple: ( 27 | "neither@does-this.com", 28 | // Error: Schema validation failed on argument.complicated-tuple.1: 29 | // String must be a valid IP address 30 | "NOT AN IP", 31 | 1, 32 | ), 33 | ), 34 | my-schema, 35 | ) 36 | ``` 37 | 38 | ## Community-lead 39 | 40 | As of version 0.2.0, `valkyrie` now resides in the typst-community organisation. Typst users are encouraged to submit additional types, assertions, coercions, and schemas that they believe are already used widely, or should be widely adopted for the health of the ecosystem. -------------------------------------------------------------------------------- /docs/manual.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst-community/valkyrie/8b47671beb30d08bd35c3c702268e17c89622dec/docs/manual.pdf -------------------------------------------------------------------------------- /docs/manual.typ: -------------------------------------------------------------------------------- 1 | #import "@preview/mantys:0.1.4": * 2 | #import "/src/lib.typ" as z 3 | 4 | #let package = toml("/typst.toml").package 5 | 6 | #show: mantys.with( 7 | ..package, 8 | title: [Valkyrie], 9 | date: datetime.today().display(), 10 | abstract: [This package implements type validation, and is targeted mainly at package and template developers. The desired outcome is that it becomes easier for the programmer to quickly put a package together without spending a long time on type safety, but also to make the usage of those packages by end-users less painful by generating useful error messages.], 11 | examples-scope: (z: z), 12 | ) 13 | 14 | #show raw: it => { 15 | show "{{VERSION}}": package.version 16 | it 17 | } 18 | 19 | = Example usage 20 | 21 | #add-type("schema", color: rgb("#bda8ed")) 22 | #add-type("z-ctx", color: rgb("#afeda8")) 23 | // #mantys.add-type("scope", color: rgb("#afeda8")) 24 | #add-type("internal", color: rgb("#ff8c8c")) 25 | 26 | #example(side-by-side: true)[```typst 27 | #let template-schema = z.dictionary(( 28 | title: z.content(), 29 | abstract: z.content(default: []), 30 | dates: z.array(z.dictionary(( 31 | type: z.content(), 32 | date: z.string() 33 | ))), 34 | paper: z.schemas.papersize(default: "a4"), 35 | authors: z.array(z.dictionary(( 36 | name: z.string(), 37 | corresponding: z.boolean(default: false), 38 | orcid: z.string(optional: true) 39 | ))), 40 | header: z.dictionary(( 41 | journal: z.content(default: [Journal Name]), 42 | article-type: z.content(default: "Article"), 43 | article-color: z.color(default: rgb(167,195,212)), 44 | article-meta: z.content(default: []) 45 | )), 46 | )); 47 | 48 | 49 | #z.parse( 50 | ( 51 | title: [This is a required title], 52 | paper: "a3", 53 | authors: ( (name: "Example"),) 54 | ), 55 | template-schema, 56 | ) 57 | 58 | ```] 59 | 60 | = Documentation 61 | == Terminology 62 | As this package introduces several type-like objects, the Tidy style has had these added for clarity. At present, these are #dtype("schema") (to represent type-validating objects), #dtype("z-ctx") (to represent the current state of the parsing heuristic), and #dtype("scope") (an array of strings that represents the parent object of values being parsed). #dtype("internal") represents arguments that, while settable by the end-user, should be reserved for internal or advanced usage. 63 | 64 | Generally, users of this package will only need to be aware of the #dtype("schema") type. 65 | 66 | == Specific language 67 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in #link("http://www.ietf.org/rfc/rfc2119.txt", [RFC 2119]). 68 | 69 | == Use cases 70 | The interface for a template that a user expects and that the developer has implemented are rearly one and the same. Instead, the user will apply common sense and the developer will put in somewhere between a token- and a whole-hearted- attempt at making their interface intuitive. Contrary to what one might expect, this makes it more difficult for the end user to correctly guess the interface as different developers will disagree on what is and isn't intuitive, and what edge cases the developer is willing to cover. 71 | 72 | By first providing a low-level set of tools for validating primitives upon which more complicated schemas can be defined, `Valkyrie` handles both the micro and macro of input validation. 73 | 74 | #pagebreak() 75 | == Parsing functions 76 | 77 | #command( 78 | "parse", 79 | arg[object], 80 | arg[schemas], 81 | arg(ctx: auto), 82 | arg(scope: ("argument",)), 83 | ret: ("any", "none"), 84 | )[ 85 | Validates an object against one or more schemas. *WILL* return the given object after validation if successful, or none and *MAY* throw a failed assertion error. 86 | 87 | #argument("object", types: "any")[ 88 | Object to validate against provided schema. Object *SHOULD* satisfy the schema requirements. An error *MAY* be produced if not. 89 | ] 90 | #argument("schemas", types: ("array", "schema"))[ 91 | Schema against which `object` is validated. Coerced into array. *MUST* be an array of valid valkyrie schema types. 92 | ] 93 | #argument("ctx", default: auto, types: "z-ctx")[ 94 | ctx passed to schema validator function, containing flags that *MAY* alter behaviour. 95 | ] 96 | #argument("scope", default: ("argument",), types: "scope")[ 97 | An array of strings used to generate the string representing the location of a failed requirement within `object`. *MUST* be an array of strings of length greater than or equal to `1` 98 | ] 99 | 100 | ] 101 | 102 | #pagebreak() 103 | == Schema definition functions 104 | For the sake of brevity and owing to their consistency, the arguments that each schema generating function accepts are listed in the table below, followed by a description of each of argument. 105 | 106 | #let rotatex(body, angle) = style(styles => { 107 | let size = measure(body, styles) 108 | box( 109 | inset: ( 110 | x: -size.width / 2 + ( 111 | size.width * calc.abs(calc.cos(angle)) + size.height * calc.abs( 112 | calc.sin(angle), 113 | ) 114 | ) / 2, 115 | y: -size.height / 2 + ( 116 | size.height * calc.abs(calc.cos(angle)) + size.width * calc.abs( 117 | calc.sin(angle), 118 | ) 119 | ) / 2, 120 | ), 121 | rotate(body, angle), 122 | ) 123 | }) 124 | 125 | #align( 126 | center, 127 | table( 128 | stroke: black + 0.75pt, 129 | columns: (1fr,) + 12 * (auto,), 130 | inset: 9pt, 131 | align: (horizon, horizon + center), 132 | table.header( 133 | [], 134 | rotatex([*any*], -90deg), 135 | rotatex([*array*], -90deg), 136 | rotatex([*boolean*], -90deg), 137 | rotatex([*color*], -90deg), 138 | rotatex([*content*], -90deg), 139 | rotatex([*date*], -90deg), 140 | rotatex([*dictionary*], -90deg), 141 | rotatex([*either*], -90deg), 142 | rotatex([*number, integer, float*], -90deg), 143 | rotatex([*string, ip, email*], -90deg), 144 | rotatex([*tuple*], -90deg), 145 | rotatex([*choice*], -90deg), 146 | ), 147 | [body], 148 | [ ], 149 | [✔], 150 | [ ], 151 | [ ], 152 | [ ], 153 | [ ], 154 | [✔], 155 | [✔], 156 | [ ], 157 | [ ], 158 | [✔], 159 | [✔], 160 | [name], 161 | [✱], 162 | [✱], 163 | [✱], 164 | [✱], 165 | [✱], 166 | [✱], 167 | [✱], 168 | [✱], 169 | [✱], 170 | [✱], 171 | [✱], 172 | [✱], 173 | [optional], 174 | [✔], 175 | [✔], 176 | [✔], 177 | [✔], 178 | [✔], 179 | [✔], 180 | [✔], 181 | [✔], 182 | [✔], 183 | [✔], 184 | [✔], 185 | [✔], 186 | [default], 187 | [✔], 188 | [✔], 189 | [✔], 190 | [✔], 191 | [✔], 192 | [✔], 193 | [✔], 194 | [✔], 195 | [✔], 196 | [✔], 197 | [✔], 198 | [✔], 199 | [types], 200 | [✔], 201 | [✱], 202 | [✱], 203 | [✱], 204 | [✱], 205 | [✱], 206 | [✱], 207 | [✱], 208 | [✱], 209 | [✱], 210 | [✱], 211 | [✱], 212 | [assertions], 213 | [✔], 214 | [✔], 215 | [✔], 216 | [✔], 217 | [✔], 218 | [✔], 219 | [✱], 220 | [✱], 221 | [✔], 222 | [✱], 223 | [✔], 224 | [✱], 225 | [pre-transform], 226 | [✔], 227 | [✔], 228 | [✔], 229 | [✔], 230 | [✔], 231 | [✔], 232 | [✱], 233 | [✱], 234 | [✔], 235 | [✔], 236 | [✔], 237 | [✔], 238 | [post-transform], 239 | [✔], 240 | [✔], 241 | [✔], 242 | [✔], 243 | [✔], 244 | [✔], 245 | [✔], 246 | [✔], 247 | [✔], 248 | [✔], 249 | [✔], 250 | [✔], 251 | ), 252 | ) 253 | 254 | ✔ Indicates that the argument is available to the user. 255 | ✱ Indicates that while the argument is available to the user, it may be used internally or may hold a default value. 256 | 257 | #pagebreak() 258 | 259 | #block( 260 | breakable: false, 261 | argument( 262 | "name", 263 | default: "unknown", 264 | types: "string", 265 | )[Human-friendly name of the schema for error-reporting purposes.], 266 | ) 267 | 268 | #block( 269 | breakable: false, 270 | argument("optional", default: false, types: "boolean")[ 271 | Allows the value to have not been set at the time of parsing, without generating an error. 272 | #mty.alert[If used on a dictionary, consider adding default values to child schemas instead.] 273 | #mty.alert[If used on a array, consider relying on the default (an empty array) instead.] 274 | ], 275 | ) 276 | 277 | #block( 278 | breakable: false, 279 | argument("default", default: none, types: "any")[ 280 | The default value to use if object being validated is `none`. 281 | #mty.alert[Setting a default value allows the end-user to omit it.] 282 | ], 283 | ) 284 | 285 | #block( 286 | breakable: false, 287 | argument( 288 | "types", 289 | default: (), 290 | types: "array", 291 | )[Array of allowable types. If not set, all types are accepted], 292 | ) 293 | 294 | #block( 295 | breakable: false, 296 | argument("assertions", default: (), types: "array")[ 297 | Array of assertions to be tested during object validation. see (LINK TO ASSERTIONS) 298 | 299 | #mty.alert[Assertions cannot modify values] 300 | ], 301 | ) 302 | 303 | #block( 304 | breakable: false, 305 | argument("pre-transform", default: "(self,it)=>it", types: "function")[ 306 | Transformation to apply prior to validation. Can be used to coerce values. 307 | 308 | ], 309 | ) 310 | 311 | #block( 312 | breakable: false, 313 | argument( 314 | "post-transform", 315 | default: "(self,it)=>it", 316 | types: "function", 317 | )[Transformation to apply after validation. Can be used to reshape values for internal use 318 | 319 | ], 320 | ) 321 | 322 | #pagebreak() 323 | #command("alignment", sarg[args], ret: "schema")[ 324 | Generates a schema that accepts only alignment objects as valid. 325 | ] 326 | 327 | #command("angle", sarg[args], ret: "schema")[ 328 | Generates a schema that accepts only angles as valid. 329 | ] 330 | 331 | #command("any", sarg[args], ret: "schema")[ 332 | Generates a schema that accepts any input as valid. 333 | ] 334 | 335 | #command("array", arg[schema], sarg[args], ret: "schema")[ 336 | #argument( 337 | "schema", 338 | types: "schema", 339 | )[Schema against which to validate child entries. Defaults to #tidyref(none, "any").] 340 | ] 341 | 342 | #command("boolean", sarg[args], ret: "schema")[ 343 | Generates a schema that accepts only booleans as valid. 344 | ] 345 | 346 | #command("bytes", sarg[args], ret: "schema")[ 347 | Generates a schema that accepts only bytes as valid. 348 | ] 349 | 350 | #command("color", sarg[args], ret: "schema")[ 351 | Generates a schema that accepts only colors as valid. 352 | ] 353 | 354 | #command("content", sarg[args], ret: "schema")[ 355 | Generates a schema that accepts only content or string as valid. 356 | ] 357 | 358 | #command("date", sarg[args], ret: "schema")[ 359 | Generates a schema that accepts only datetime objects as valid. 360 | ] 361 | 362 | #command( 363 | "dictionary", 364 | arg(aliases: (:)), 365 | arg[schema], 366 | sarg[args], 367 | ret: "schema", 368 | )[ 369 | #argument( 370 | "aliases", 371 | types: "dict", 372 | default: (:), 373 | )[Dictionary representation of source to destination aliasing. Has the effect of allowing the user to key something with `source` when its `destination` that is meant.] 374 | #argument( 375 | "schema", 376 | types: "dictionary", 377 | )[Dictionary of schema elements, used to define the validation rules for each entry.] 378 | ] 379 | 380 | #command("direction", sarg[args], ret: "schema")[ 381 | Generates a schema that accepts only directions as valid. 382 | ] 383 | 384 | #command("either", sarg[schema], sarg[args], ret: "schema")[ 385 | #argument( 386 | "schema", 387 | types: "dictionary", 388 | is-sink: true, 389 | )[Positional arguments of validation schemes in order or preference that an input value should satisfy.] 390 | ] 391 | 392 | #command("function", sarg[args], ret: "schema")[ 393 | Generates a schema that accepts only functions as valid. 394 | ] 395 | 396 | #command("fraction", sarg[args], ret: "schema")[ 397 | Generates a schema that accepts only fractions as valid. 398 | ] 399 | 400 | #command("gradient", sarg[args], ret: "schema")[ 401 | Generates a schema that accepts only gradient objects as valid. 402 | ] 403 | 404 | #command("label", sarg[args], ret: "schema")[ 405 | Generates a schema that accepts only labels as valid. 406 | ] 407 | 408 | #command("length", sarg[args], ret: "schema")[ 409 | Generates a schema that accepts only lengths as valid. 410 | ] 411 | 412 | #command("location", sarg[args], ret: "schema")[ 413 | Generates a schema that accepts only locations as valid. 414 | ] 415 | 416 | #command("number", arg(min: none), arg(max: none), sarg[args], ret: "schema")[ 417 | Generates a schema that accepts only numbers as valid. 418 | ] 419 | 420 | #command("plugin", sarg[args], ret: "schema")[ 421 | Generates a schema that accepts only plugins as valid. 422 | ] 423 | 424 | #command("ratio", sarg[args], ret: "schema")[ 425 | Generates a schema that accepts only ratios as valid. 426 | ] 427 | 428 | #command("relative", sarg[args], ret: "schema")[ 429 | Generates a schema that accepts only relative types, lengths, or ratios as valid. 430 | ] 431 | 432 | #command("regex", sarg[args], ret: "schema")[ 433 | Generates a schema that accepts only regex expressions as valid. 434 | ] 435 | 436 | #command("selector", sarg[args], ret: "schema")[ 437 | Generates a schema that accepts only selectors as valid. 438 | ] 439 | 440 | #command("string", arg(min: none), arg(max: none), sarg[args], ret: "schema")[ 441 | Generates a schema that accepts only strings as valid. 442 | ] 443 | 444 | #command("stroke", sarg[args], ret: "schema")[ 445 | Generates a schema that accepts only stroke objects as valid. 446 | ] 447 | 448 | #command("symbol", sarg[args], ret: "schema")[ 449 | Generates a schema that accepts only symbol types as valid. 450 | ] 451 | 452 | #command("tuple", sarg[schema], sarg[args], ret: "schema")[ 453 | #argument( 454 | "schema", 455 | types: "schema", 456 | is-sink: true, 457 | )[Positional arguments of validation schemes representing a tuple.] 458 | ] 459 | 460 | #command("version", sarg[args], ret: "schema")[ 461 | Generates a schema that accepts only version objects as valid. 462 | ] 463 | 464 | #command( 465 | "sink", 466 | arg(positional: none), 467 | arg(named: none), 468 | sarg[args], 469 | ret: "schema", 470 | )[ 471 | #argument( 472 | "positional", 473 | types: ("schema", none), 474 | )[Schema that `args.pos()` must satisfy. If `none`, no positional arguments may be present] 475 | #argument( 476 | "named", 477 | types: ("schema", none), 478 | )[Schema that `args.named()` must satisfy. If `none`, no named arguments may be present] 479 | ] 480 | 481 | #command("choice", arg[choices], sarg[args], ret: "schema")[ 482 | #argument("choices", types: "array")[Array of valid inputs] 483 | ] 484 | 485 | #pagebreak() 486 | 487 | #import "@preview/tidy:0.2.0" 488 | 489 | #let module-doc = tidy.parse-module( 490 | read("/src/coercions.typ"), 491 | name: "z.coerce", 492 | label-prefix: "z.coerce", 493 | scope: (:), 494 | ) 495 | 496 | #tidy.show-module( 497 | module-doc, 498 | style: ( 499 | get-type-color: mty-tidy.get-type-color, 500 | show-outline: mty-tidy.show-outline, 501 | show-parameter-list: mty-tidy.show-parameter-list, 502 | show-parameter-block: mty-tidy.show-parameter-block, 503 | show-function: mty-tidy.show-function.with( 504 | tidy: tidy, 505 | extract-headings: true, 506 | ), 507 | show-variable: mty-tidy.show-variable.with(tidy: tidy), 508 | show-example: mty-tidy.show-example, 509 | show-reference: mty-tidy.show-reference, 510 | ), 511 | first-heading-level: 2, 512 | show-module-name: true, 513 | sort-functions: false, 514 | show-outline: true, 515 | ) 516 | 517 | #tidy-module(read("/src/coercions.typ"), name: "coerce") 518 | #pagebreak() 519 | 520 | #let module-doc = tidy.parse-module( 521 | read("/src/assertions.typ") + read("/src/assertions/comparative.typ") + read("/src/assertions/string.typ"), 522 | name: "z.assert", 523 | label-prefix: "z.assert", 524 | scope: (:), 525 | ) 526 | 527 | #tidy.show-module( 528 | module-doc, 529 | style: ( 530 | get-type-color: mty-tidy.get-type-color, 531 | show-outline: mty-tidy.show-outline, 532 | show-parameter-list: mty-tidy.show-parameter-list, 533 | show-parameter-block: mty-tidy.show-parameter-block, 534 | show-function: mty-tidy.show-function.with( 535 | tidy: tidy, 536 | extract-headings: true, 537 | ), 538 | show-variable: mty-tidy.show-variable.with(tidy: tidy), 539 | show-example: mty-tidy.show-example, 540 | show-reference: mty-tidy.show-reference, 541 | ), 542 | first-heading-level: 2, 543 | show-module-name: true, 544 | sort-functions: false, 545 | show-outline: true, 546 | ) 547 | 548 | #let module-doc = tidy.parse-module( 549 | read("/src/assertions/length.typ"), 550 | name: "z.assert.length", 551 | label-prefix: "z.assert.string.", 552 | scope: (:), 553 | ) 554 | 555 | #tidy.show-module( 556 | module-doc, 557 | style: ( 558 | get-type-color: mty-tidy.get-type-color, 559 | show-outline: mty-tidy.show-outline, 560 | show-parameter-list: mty-tidy.show-parameter-list, 561 | show-parameter-block: mty-tidy.show-parameter-block, 562 | show-function: mty-tidy.show-function.with( 563 | tidy: tidy, 564 | extract-headings: true, 565 | ), 566 | show-variable: mty-tidy.show-variable.with(tidy: tidy), 567 | show-example: mty-tidy.show-example, 568 | show-reference: mty-tidy.show-reference, 569 | ), 570 | first-heading-level: 2, 571 | show-module-name: true, 572 | sort-functions: false, 573 | show-outline: true, 574 | ) 575 | 576 | #pagebreak() 577 | = Advanced Documentation 578 | == Validation heuristic 579 | 580 | #import "@preview/fletcher:0.4.4" as fletcher: diagram, node, edge, shapes 581 | 582 | #figure( 583 | align( 584 | center, 585 | diagram( 586 | spacing: 2em, 587 | node-stroke: 0.75pt, 588 | edge-stroke: 0.75pt, 589 | node((-2, 1), [Start], corner-radius: 2pt, shape: shapes.circle), 590 | edge("-|>"), 591 | node((0, 1), align(center)[`value` or `self.default`]), 592 | edge("-|>"), 593 | node((0, 2), align(center)[pre-transform value], corner-radius: 2pt), 594 | edge("-|>"), 595 | node((0, 3), align(center)[Assert type of value], corner-radius: 2pt), 596 | node( 597 | (-1, 4), 598 | align(center)[Allow #repr(none) if \ `self.optional` is #true], 599 | corner-radius: 2pt, 600 | ), 601 | node( 602 | (0, 4), 603 | align(center)[Allow if `self.types` \ length is 0], 604 | corner-radius: 2pt, 605 | ), 606 | node( 607 | (1, 4), 608 | align(center)[Allow `value` if type\ in `self.types`], 609 | corner-radius: 2pt, 610 | ), 611 | node((1, 3), align(center)[`self.fail-validation`], corner-radius: 2pt), 612 | edge("-|>"), 613 | node( 614 | (2, 3), 615 | align(center)[throw], 616 | corner-radius: 2pt, 617 | shape: shapes.circle, 618 | ), 619 | edge((0, 3), (-1, 4), "-|>", bend: -20deg), 620 | edge((-1, 4), (0, 5), "-|>", bend: -20deg), 621 | edge((0, 3), (0, 4), "-|>"), 622 | edge((0, 4), (0, 5), "-|>"), 623 | edge((0, 3), (1, 4), "-|>", bend: 20deg), 624 | edge((1, 4), (0, 5), "-|>", bend: 20deg), 625 | edge((0, 3), (1, 3), "-|>"), 626 | node( 627 | (0, 5), 628 | align(center)[Handle descendents \ transformation], 629 | corner-radius: 2pt, 630 | ), 631 | edge( 632 | "ll,uuuu", 633 | "|>--|>", 634 | align(center)[child schema \ on descendent], 635 | label-side: left, 636 | label-pos: 0.3, 637 | ), 638 | edge("-|>"), 639 | node( 640 | (0, 6), 641 | align(center)[Handle assertions \ transformation], 642 | corner-radius: 2pt, 643 | ), 644 | edge("-|>"), 645 | node((1, 6), align(center)[post-transform `value`], corner-radius: 2pt), 646 | edge("-|>"), 647 | node((2, 6), [end], corner-radius: 2pt, shape: shapes.circle), 648 | ) + v(2em), 649 | ), 650 | caption: [Flow diagram representation of parsing heuristic when validating a value against a schema.], 651 | ) 652 | -------------------------------------------------------------------------------- /scripts/package: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eu 3 | 4 | # adapted from https://github.com/johannes-wolf/cetz/blob/35c0868378cea5ad323cc0d9c2f76de8ed9ba5bd/scripts/package 5 | # licensed under Apache License 2.0 6 | 7 | . "$(dirname "${BASH_SOURCE[0]}")/setup" 8 | 9 | if (( $# < 1 )) || [[ "${1:-}" == "help" ]]; then 10 | echo "package TARGET" 11 | echo "" 12 | echo "Packages all relevant files into a directory named '/'" 13 | echo "at TARGET. If TARGET is set to @local or @preview, the local Typst package" 14 | echo "directory will be used so that the package gets installed for local use." 15 | echo "The name and version are read from 'typst.toml' in the project root." 16 | echo "" 17 | echo "Local package prefix: $DATA_DIR/typst/package/local" 18 | echo "Local preview package prefix: $DATA_DIR/typst/package/preview" 19 | exit 1 20 | fi 21 | 22 | TARGET="$(resolve-target "${1:?Missing target path, @local or @preview}")" 23 | echo "Install dir: $TARGET" 24 | 25 | # ignore rules 26 | readarray -t ignores < <(grep -v '^#' .typstignore | grep '[^[:blank:]]') 27 | 28 | # recursively print all files that are not excluded via .typstignore 29 | function enumerate { 30 | local root="$1" 31 | if [[ -f "$root" ]]; then 32 | echo "$root" 33 | else 34 | local files 35 | readarray -t files < <(find "$root" \ 36 | -mindepth 1 -maxdepth 1 \ 37 | -not -name .git \ 38 | -not -name .typstignore) 39 | # declare -p files >&2 40 | 41 | local f 42 | for f in "${files[@]}"; do 43 | local include 44 | include=1 45 | 46 | local ignore 47 | for ignore in "${ignores[@]}"; do 48 | if [[ "$ignore" =~ ^! ]]; then 49 | ignore="${ignore:1}" 50 | if [[ "$f" == ./$ignore ]]; then 51 | # echo "\"$f\" matched \"!$ignore\"" >&2 52 | include=1 53 | fi 54 | elif [[ "$f" == ./$ignore ]]; then 55 | # echo "\"$f\" matched \"$ignore\"" >&2 56 | include=0 57 | fi 58 | done 59 | if [[ "$include" == 1 ]]; then 60 | enumerate "$f" 61 | fi 62 | done 63 | fi 64 | } 65 | 66 | # List of all files that get packaged 67 | readarray -t files < <(enumerate ".") 68 | # declare -p files >&2 69 | 70 | TMP="$(mktemp -d)" 71 | 72 | for f in "${files[@]}"; do 73 | mkdir -p "$TMP/$(dirname "$f")" 2>/dev/null 74 | cp -r "$ROOT/$f" "$TMP/$f" 75 | done 76 | 77 | TARGET="${TARGET:?}/${PKG_PREFIX:?}/${VERSION:?}" 78 | echo "Packaged to: $TARGET" 79 | if rm -r "${TARGET:?}" 2>/dev/null; then 80 | echo "Overwriting existing version." 81 | fi 82 | mkdir -p "$TARGET" 83 | 84 | # include hidden files by setting dotglob 85 | shopt -s dotglob 86 | mv "$TMP"/* "$TARGET" 87 | -------------------------------------------------------------------------------- /scripts/setup: -------------------------------------------------------------------------------- 1 | # source this script to prepare some common environment variables 2 | 3 | # adapted from https://github.com/johannes-wolf/cetz/blob/35c0868378cea5ad323cc0d9c2f76de8ed9ba5bd/scripts/package 4 | # licensed under Apache License 2.0 5 | 6 | # Local package directories per platform 7 | if [[ "$OSTYPE" == "linux"* ]]; then 8 | DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}" 9 | elif [[ "$OSTYPE" == "darwin"* ]]; then 10 | DATA_DIR="$HOME/Library/Application Support" 11 | else 12 | DATA_DIR="${APPDATA}" 13 | fi 14 | 15 | function read-toml() { 16 | local file="$1" 17 | local key="$2" 18 | # Read a key value pair in the format: = "" 19 | # stripping surrounding quotes. 20 | perl -lne "print \"\$1\" if /^${key}\\s*=\\s*\"(.*)\"/" < "$file" 21 | } 22 | 23 | ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")"; pwd -P)/.." # macOS has no realpath 24 | PKG_PREFIX="$(read-toml "$ROOT/typst.toml" "name")" 25 | VERSION="$(read-toml "$ROOT/typst.toml" "version")" 26 | 27 | function resolve-target() { 28 | local target="$1" 29 | 30 | if [[ "$target" == "@local" ]]; then 31 | echo "${DATA_DIR}/typst/packages/local" 32 | elif [[ "$target" == "@preview" ]]; then 33 | echo "${DATA_DIR}/typst/packages/preview" 34 | else 35 | echo "$target" 36 | fi 37 | } 38 | -------------------------------------------------------------------------------- /scripts/uninstall: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eu 3 | 4 | # adapted from https://github.com/johannes-wolf/cetz/blob/35c0868378cea5ad323cc0d9c2f76de8ed9ba5bd/scripts/package 5 | # licensed under Apache License 2.0 6 | 7 | . "$(dirname "${BASH_SOURCE[0]}")/setup" 8 | 9 | if (( $# < 1 )) || [[ "${1:-}" == "help" ]]; then 10 | echo "uninstall TARGET" 11 | echo "" 12 | echo "Removes the package installed into a directory named '/'" 13 | echo "at TARGET. If TARGET is set to @local or @preview, the local Typst package" 14 | echo "directory will be used so that the package gets installed for local use." 15 | echo "The name and version are read from 'typst.toml' in the project root." 16 | echo "" 17 | echo "Local package prefix: $DATA_DIR/typst/package/local" 18 | echo "Local preview package prefix: $DATA_DIR/typst/package/preview" 19 | exit 1 20 | fi 21 | 22 | TARGET="$(resolve-target "${1:?Missing target path, @local or @preview}")" 23 | echo "Install dir: $TARGET" 24 | 25 | TARGET="${TARGET:?}/${PKG_PREFIX:?}/${VERSION:?}" 26 | echo "Package to uninstall: $TARGET" 27 | if [[ ! -e "${TARGET:?}" ]]; then 28 | echo "Package was not found." 29 | elif rm -r "${TARGET:?}" 2>/dev/null; then 30 | echo "Successfully removed." 31 | else 32 | echo "Removal failed." 33 | fi 34 | -------------------------------------------------------------------------------- /src/assertions-util.typ: -------------------------------------------------------------------------------- 1 | 2 | #let assert-base-type(arg, scope: ("arguments",)) = { 3 | assert( 4 | "valkyrie-type" in arg, 5 | message: "Invalid valkyrie type in " + scope.join("."), 6 | ) 7 | } 8 | 9 | #let assert-base-type-array(arg, scope: ("arguments",)) = { 10 | for (name, value) in arg.enumerate() { 11 | assert-base-type(value, scope: (..scope, str(name))) 12 | } 13 | } 14 | 15 | #let assert-base-type-dictionary(arg, scope: ("arguments",)) = { 16 | for (name, value) in arg { 17 | assert-base-type(value, scope: (..scope, name)) 18 | } 19 | } 20 | 21 | #let assert-base-type-arguments(arg, scope: ("arguments",)) = { 22 | for (name, value) in arg.named() { 23 | assert-base-type(value, scope: (..scope, name)) 24 | } 25 | 26 | for (pos, value) in arg.pos().enumerate() { 27 | assert-base-type(value, scope: (..scope, "[" + pos + "]")) 28 | } 29 | } 30 | 31 | #let assert-types(var, types: (), default: none, name: "") = { 32 | assert( 33 | type(var) in (type(default), ..types), 34 | message: "" + name + " must be of type " + types.map(str).join( 35 | ", ", 36 | last: " or ", 37 | ) + ". Got " + str(type(var)), 38 | ) 39 | } 40 | 41 | #let assert-soft(var, condition: () => true, message: "") = { 42 | if (var != none) { 43 | assert(condition(var), message: message) 44 | } 45 | } 46 | 47 | #let assert-positive(var, name: "") = { 48 | assert-soft( 49 | var, 50 | condition: var => var >= 0, 51 | message: name + " must be positive", 52 | ) 53 | } 54 | 55 | #let assert-positive-type(var, name: "", types: (float, int), default: none) = { 56 | assert-types(var, types: types, default: default, name: name) 57 | assert-positive(var, name: name) 58 | } 59 | 60 | #let assert-boilerplate-params( 61 | default: none, 62 | assertions: none, 63 | pre-transform: none, 64 | post-transform: none, 65 | ) = { 66 | if (assertions != none) { 67 | assert-types(assertions, types: (type(()),), name: "Assertions") 68 | } 69 | if (pre-transform != none) { 70 | assert-types(pre-transform, types: (function,), name: "Pre-transform") 71 | } 72 | if (post-transform != none) { 73 | assert-types(post-transform, types: (function,), name: "Post-transform") 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/assertions.typ: -------------------------------------------------------------------------------- 1 | #import "./assertions/length.typ" 2 | #import "./assertions/comparative.typ": min, max, eq, neq 3 | #import "./assertions/string.typ": * 4 | 5 | /// Asserts that the given value is contained within the provided list. Useful for complicated enumeration types. 6 | /// - list (array): An array of inputs considered valid. 7 | #let one-of(list) = ( 8 | condition: (self, it) => { 9 | list.contains(it) 10 | }, 11 | message: (self, it) => "Unknown " + self.name + " `" + repr(it) + "`", 12 | ) 13 | -------------------------------------------------------------------------------- /src/assertions/comparative.typ: -------------------------------------------------------------------------------- 1 | #import "../assertions-util.typ": assert-positive-type 2 | 3 | /// Asserts that tested value is greater than or equal to argument 4 | #let min(rhs) = { 5 | assert-positive-type(rhs, types: (int,), name: "Minimum") 6 | 7 | return ( 8 | condition: (self, it) => it >= rhs, 9 | message: (self, it) => "Must be at least " + str(rhs), 10 | ) 11 | } 12 | 13 | /// Asserts that tested value is less than or equal to argument 14 | #let max(rhs) = { 15 | assert-positive-type(rhs, types: (int,), name: "Maximum") 16 | 17 | return ( 18 | condition: (self, it) => it <= rhs, 19 | message: (self, it) => "Must be at most " + str(rhs), 20 | ) 21 | } 22 | 23 | /// Asserts that tested value is exactly equal to argument 24 | #let eq(arg) = { 25 | assert-positive-type(arg, types: (int,), name: "Equality") 26 | 27 | return ( 28 | condition: (self, it) => it == arg, 29 | message: (self, it) => "Must be exactly " + str(arg), 30 | ) 31 | } 32 | 33 | /// Asserts that tested value is not exactly equal to argument 34 | #let neq(arg) = { 35 | assert-positive-type(arg, types: (int,), name: "Equality") 36 | 37 | return ( 38 | condition: (self, it) => it != arg, 39 | message: (self, it) => "Must not equal " + str(arg), 40 | ) 41 | } -------------------------------------------------------------------------------- /src/assertions/length.typ: -------------------------------------------------------------------------------- 1 | #import "../assertions-util.typ": assert-positive-type 2 | 3 | /// Asserts that tested value's length is greater than or equal to argument 4 | #let min(rhs) = { 5 | assert-positive-type(rhs, types: (int,), name: "Minimum length") 6 | 7 | return ( 8 | condition: (self, it) => it.len() >= rhs, 9 | message: (self, it) => "Length must be at least " + str(rhs), 10 | ) 11 | } 12 | 13 | /// Asserts that tested value's length is less than or equal to argument 14 | #let max(rhs) = { 15 | assert-positive-type(rhs, types: (int,), name: "Maximum length") 16 | 17 | return ( 18 | condition: (self, it) => it.len() <= rhs, 19 | message: (self, it) => "Length must be at most " + str(rhs), 20 | ) 21 | } 22 | 23 | /// Asserts that tested value's length is exactly equal to argument 24 | #let equals(arg) = { 25 | assert-positive-type(arg, types: (int,), name: "Exact length") 26 | 27 | return ( 28 | condition: (self, it) => it.len() == arg, 29 | message: (self, it) => "Length must equal " + str(arg), 30 | ) 31 | } 32 | 33 | /// Asserts that tested value's length is not equal to argument 34 | #let neq(arg) = { 35 | assert-positive-type(arg, types: (int,), name: "Exact length") 36 | 37 | return ( 38 | condition: (self, it) => it.len() != rhs, 39 | message: (self, it) => "Length must not equal " + str(rhs), 40 | ) 41 | } -------------------------------------------------------------------------------- /src/assertions/string.typ: -------------------------------------------------------------------------------- 1 | /// Asserts that a tested value contains the argument (string) 2 | #let contains(value) = { 3 | return ( 4 | condition: (self, it) => it.contains(value), 5 | message: (self, it) => "Must contain " + str(value), 6 | ) 7 | } 8 | 9 | /// Asserts that a tested value starts with the argument (string) 10 | #let starts-with(value) = { 11 | return ( 12 | condition: (self, it) => it.starts-with(value), 13 | message: (self, it) => "Must start with " + str(value), 14 | ) 15 | } 16 | 17 | /// Asserts that a tested value ends with the argument (string) 18 | #let ends-with(value) = { 19 | return ( 20 | condition: (self, it) => it.ends-with(value), 21 | message: (self, it) => "Must end with " + str(value), 22 | ) 23 | } 24 | 25 | /// Asserts that a tested value matches with the needle argument (string) 26 | #let matches(needle, message: (self, it) => { }) = { 27 | return ( 28 | condition: (self, it) => it.match(needle) != none, 29 | message: message, 30 | ) 31 | } -------------------------------------------------------------------------------- /src/base-type.typ: -------------------------------------------------------------------------------- 1 | #import "ctx.typ": z-ctx 2 | #import "assertions-util.typ": assert-boilerplate-params 3 | 4 | /// Schema generator. Provides default values for when defining custom types. 5 | #let base-type( 6 | name: "unknown", 7 | description: none, 8 | optional: false, 9 | default: none, 10 | types: (), 11 | assertions: (), 12 | pre-transform: (self, it) => it, 13 | post-transform: (self, it) => it, 14 | ) = { 15 | 16 | assert-boilerplate-params( 17 | assertions: assertions, 18 | pre-transform: pre-transform, 19 | post-transform: post-transform, 20 | ) 21 | 22 | return ( 23 | valkyrie-type: true, 24 | name: name, 25 | description: if (description != none){ description } else { name }, 26 | optional: optional, 27 | default: default, 28 | types: types, 29 | assertions: assertions, 30 | pre-transform: pre-transform, 31 | post-transform: post-transform, 32 | assert-type: (self, it, scope: (), ctx: z-ctx(), types: ()) => { 33 | 34 | if ((self.optional) and (it == none)) { 35 | return true 36 | } 37 | 38 | if (self.types.len() == 0) { 39 | return true 40 | } 41 | 42 | if (type(it) not in self.types) { 43 | (self.fail-validation)( 44 | self, 45 | it, 46 | scope: scope, 47 | ctx: ctx, 48 | message: "Expected " + self.types.map(repr).join( 49 | ", ", 50 | last: " or ", 51 | ) + ". Got " + str(type(it)), 52 | ) 53 | return false 54 | } 55 | 56 | true 57 | }, 58 | handle-assertions: (self, it, scope: (), ctx: z-ctx()) => { 59 | for (key, value) in self.assertions.enumerate() { 60 | if (value.at("precondition", default: none) != none) { 61 | if (self.at(value.precondition, default: none) == none) { 62 | continue 63 | } 64 | } 65 | if not (it == none or (value.condition)(self, it)) { 66 | (self.fail-validation)( 67 | self, 68 | it, 69 | ctx: ctx, 70 | scope: scope, 71 | message: (value.message)(self, it), 72 | ) 73 | return self.default 74 | } 75 | } 76 | it 77 | }, 78 | handle-descendents: (self, it, ctx: z-ctx(), scope: ()) => { 79 | it 80 | }, 81 | validate: (self, it, scope: (), ctx: z-ctx()) => { 82 | 83 | //it = self.default 84 | if (it == none or it == auto) { 85 | it = self.default 86 | } 87 | it = (self.pre-transform)(self, it) 88 | 89 | // assert types 90 | if ( 91 | not (self.assert-type)( 92 | self, 93 | it, 94 | scope: scope, 95 | ctx: ctx, 96 | types: self.types, 97 | ) 98 | ) { 99 | return none 100 | } 101 | 102 | it = (self.handle-descendents)(self, it, scope: scope, ctx: ctx) 103 | 104 | // Custom assertions 105 | it = (self.handle-assertions)(self, it, scope: scope, ctx: ctx) 106 | 107 | (self.post-transform)(self, it) 108 | }, 109 | fail-validation: (self, it, scope: (), ctx: z-ctx(), message: "") => { 110 | let display = "Schema validation failed on " + scope.join(".") 111 | if message.len() > 0 { 112 | display += ": " + message 113 | } 114 | ctx.outcome = display 115 | assert(ctx.soft-error, message: display) 116 | return none 117 | }, 118 | ) 119 | } 120 | -------------------------------------------------------------------------------- /src/coercions.typ: -------------------------------------------------------------------------------- 1 | 2 | /// If the tested value is not already of dictionary type, the function provided as argument is expected to return a dictionary type with a shape that passes validation. 3 | /// 4 | /// #example[``` 5 | /// #let schema = z.dictionary( 6 | /// pre-transform: z.coerce.dictionary((it)=>(name: it)), 7 | /// (name: z.string()) 8 | /// ) 9 | /// 10 | /// #z.parse("Hello", schema) \ 11 | /// #z.parse((name: "Hello"), schema) 12 | /// ```] 13 | /// 14 | /// - fn (function): Transformation function that the tested value and returns a dictionary that has a shape that passes validation. 15 | #let dictionary(fn) = (self, it) => { 16 | if (type(it) != type((:))) { 17 | return fn(it) 18 | } 19 | it 20 | } 21 | 22 | /// If the tested value is not already of array type, it is transformed into an array of size 1 23 | /// 24 | /// #example[``` 25 | /// #let schema = z.array( 26 | /// pre-transform: z.coerce.array, 27 | /// z.string() 28 | /// ) 29 | /// 30 | /// #z.parse("Hello", schema) \ 31 | /// #z.parse(("Hello", "world"), schema) 32 | /// ```] 33 | #let array(self, it) = { 34 | if (type(it) != type(())) { 35 | return (it,) 36 | } 37 | it 38 | } 39 | 40 | /// Tested value is forcibly converted to content type 41 | /// 42 | /// #example[``` 43 | /// #let schema = z.content( 44 | /// pre-transform: z.coerce.content 45 | /// ) 46 | /// 47 | /// #type(z.parse("Hello", schema)) \ 48 | /// #type(z.parse(123456, schema)) 49 | /// ```] 50 | #let content(self, it) = [#it] 51 | 52 | /// An attempt is made to convert string, numeric, or dictionary inputs into datetime objects 53 | /// 54 | /// #example[``` 55 | /// #let schema = z.date( 56 | /// pre-transform: z.coerce.date 57 | /// ) 58 | /// 59 | /// #z.parse(2020, schema) \ 60 | /// #z.parse("2020-03-15", schema) \ 61 | /// #z.parse("2020/03/15", schema) \ 62 | /// #z.parse((year: 2020, month: 3, day: 15), schema) \ 63 | /// ```] 64 | #let date(self, it) = { 65 | if (type(it) == type(datetime.today())) { 66 | return it 67 | } 68 | if (type(it) == int) { 69 | // assume this is the year 70 | assert( 71 | it > 1000 and it < 3000, 72 | message: "The date is assumed to be a year between 1000 and 3000", 73 | ) 74 | return datetime(year: it, month: 1, day: 1) 75 | } 76 | 77 | if (type(it) == str) { 78 | let yearMatch = it.find(regex(`^([1|2])([0-9]{3})$`.text)) 79 | if (yearMatch != none) { 80 | // This isn't awesome, but probably fine 81 | return datetime(year: int(it), month: 1, day: 1) 82 | } 83 | let dateMatch = it.find( 84 | regex(`^([1|2])([0-9]{3})([-\/])([0-9]{1,2})([-\/])([0-9]{1,2})$`.text), 85 | ) 86 | if (dateMatch != none) { 87 | let parts = it.split(regex("[-\/]")) 88 | return datetime( 89 | year: int(parts.at(0)), 90 | month: int(parts.at(1)), 91 | day: int(parts.at(2)), 92 | ) 93 | } 94 | panic("Unknown datetime object from string, try: `2020/03/15` as YYYY/MM/DD, also accepts `2020-03-15`") 95 | } 96 | 97 | if (type(it) == type((:))) { 98 | if ("year" in it) { 99 | return return datetime( 100 | year: it.at("year"), 101 | month: it.at("month", default: 1), 102 | day: it.at("day", default: 1), 103 | ) 104 | } 105 | panic("Unknown datetime object from dictionary, try: `(year: 2022, month: 2, day: 3)`") 106 | } 107 | panic("Unknown date of type '" + type(it) + "' accepts: datetime, str, int, and object") 108 | 109 | } -------------------------------------------------------------------------------- /src/ctx.typ: -------------------------------------------------------------------------------- 1 | #let ctx-proto = ( 2 | strict: false, 3 | soft-error: false, 4 | remove-optional-none: false, 5 | ) 6 | 7 | /// This is a utility function for setting contextual flags that are used during validation of objects against schemas. 8 | /// 9 | /// Currently, the following flags are described within the API: 10 | /// / strict: If set, this flag adds the requirement that there are no entries in dictionary types that are not described by the validation schema. 11 | /// / soft-error: If set, this flag silences errors from failed validation parses. It is used internally for types that should not error on validation failures. See @@either 12 | /// 13 | /// - parent (z-ctx, none): Current context (if present), to which contextual 14 | /// flags passed in variadic arguments are appended. 15 | /// - ..args (arguments): Variadic contextual flags to set. Positional arguments are discarded. 16 | /// -> z-ctx 17 | #let z-ctx(parent: (:), ..args) = ctx-proto + parent + args.named() 18 | -------------------------------------------------------------------------------- /src/lib.typ: -------------------------------------------------------------------------------- 1 | #import "types.typ": * 2 | #import "ctx.typ": z-ctx 3 | #import "base-type.typ": base-type 4 | #import "assertions-util.typ" as advanced 5 | #import "assertions.typ" as assert 6 | #import "coercions.typ" as coerce 7 | #import "schemas.typ" 8 | 9 | #let parse( 10 | object, 11 | schemas, 12 | ctx: z-ctx(), 13 | scope: ("argument",), 14 | ) = { 15 | // don't expose to external 16 | import "assertions-util.typ": assert-base-type 17 | 18 | 19 | // Validate named arguments 20 | 21 | if (type(schemas) != type(())) { 22 | schemas = (schemas,) 23 | } 24 | advanced.assert-base-type-array(schemas, scope: scope) 25 | 26 | for schema in schemas { 27 | object = (schema.validate)( 28 | schema, 29 | ctx: ctx, 30 | scope: scope, 31 | object, 32 | ) 33 | } 34 | 35 | return object 36 | } 37 | 38 | -------------------------------------------------------------------------------- /src/schemas.typ: -------------------------------------------------------------------------------- 1 | #import "schemas/author.typ": author 2 | #import "schemas/enumerations.typ": papersize -------------------------------------------------------------------------------- /src/schemas/author.typ: -------------------------------------------------------------------------------- 1 | #import "../types.typ" as z; 2 | #import "../coercions.typ" as coerce; 3 | 4 | #let author = z.dictionary( 5 | aliases: ( 6 | "affiliation": "affiliations", 7 | "website": "url", 8 | "homepage": "url", 9 | "ORCID": "orcid", 10 | "equal_contributor": "equal-contributor", 11 | "equalContributor": "equal-contributor", 12 | ), 13 | ( 14 | name: z.string(), 15 | url: z.string(optional: true), 16 | phone: z.string(optional: true), 17 | fax: z.string(optional: true), 18 | orcid: z.string(optional: true), 19 | note: z.string(optional: true), 20 | email: z.email(optional: true), 21 | corresponding: z.boolean(optional: true), 22 | equal-contributor: z.boolean(optional: true), 23 | deceased: z.boolean(optional: true), 24 | roles: z.array(z.string(), pre-transform: coerce.array), 25 | affiliations: z.array( 26 | z.either( 27 | z.string(), 28 | z.number(), 29 | ), 30 | pre-transform: coerce.array, 31 | ), 32 | ), 33 | pre-transform: coerce.dictionary(it => (name: it)), 34 | post-transform: (self, it) => { 35 | if (it.at("email", default: none) != none and "corresponding" not in it) { 36 | it.insert("corresponding", true) 37 | } 38 | return it 39 | }, 40 | ) 41 | -------------------------------------------------------------------------------- /src/schemas/enumerations.typ: -------------------------------------------------------------------------------- 1 | #import "../types.typ" as z; 2 | 3 | #let papersize = z.choice.with( 4 | description: "paper size", 5 | ( 6 | "a0", 7 | "a1", 8 | "a2", 9 | "a3", 10 | "a4", 11 | "a5", 12 | "a6", 13 | "a7", 14 | "a8", 15 | "a9", 16 | "a10", 17 | "a11", 18 | "iso-b1", 19 | "iso-b2", 20 | "iso-b3", 21 | "iso-b4", 22 | "iso-b5", 23 | "iso-b6", 24 | "iso-b7", 25 | "iso-b8", 26 | "iso-c3", 27 | "iso-c4", 28 | "iso-c5", 29 | "iso-c6", 30 | "iso-c7", 31 | "iso-c8", 32 | "din-d3", 33 | "din-d4", 34 | "din-d5", 35 | "din-d6", 36 | "din-d7", 37 | "din-d8", 38 | "sis-g5", 39 | "sis-e5", 40 | "ansi-a", 41 | "ansi-b", 42 | "ansi-c", 43 | "ansi-d", 44 | "ansi-e", 45 | "arch-a", 46 | "arch-b", 47 | "arch-c", 48 | "arch-d", 49 | "arch-e1", 50 | "arch-e", 51 | "jis-b0", 52 | "jis-b1", 53 | "jis-b2", 54 | "jis-b3", 55 | "jis-b4", 56 | "jis-b5", 57 | "jis-b6", 58 | "jis-b7", 59 | "jis-b8", 60 | "jis-b9", 61 | "jis-b10", 62 | "jis-b11", 63 | "sac-d0", 64 | "sac-d1", 65 | "sac-d2", 66 | "sac-d3", 67 | "sac-d4", 68 | "sac-d5", 69 | "sac-d6", 70 | "iso-id-1", 71 | "iso-id-2", 72 | "iso-id-3", 73 | "asia-f4", 74 | "jp-shiroku-ban-4", 75 | "jp-shiroku-ban-5", 76 | "jp-shiroku-ban-6", 77 | "jp-kiku-4", 78 | "jp-kiku-5", 79 | "jp-business-card", 80 | "cn-business-card", 81 | "eu-business-card", 82 | "fr-tellière", 83 | "fr-couronne-écriture", 84 | "fr-couronne-édition", 85 | "fr-raisin", 86 | "fr-carré", 87 | "fr-jésus", 88 | "uk-brief", 89 | "uk-draft", 90 | "uk-foolscap", 91 | "uk-quarto", 92 | "uk-crown", 93 | "uk-book-a", 94 | "uk-book-b", 95 | "us-letter", 96 | "us-legal", 97 | "us-tabloid", 98 | "us-executive", 99 | "us-foolscap-folio", 100 | "us-statement", 101 | "us-ledger", 102 | "us-oficio", 103 | "us-gov-letter", 104 | "us-gov-legal", 105 | "us-business-card", 106 | "us-digest", 107 | "us-trade", 108 | "newspaper-compact", 109 | "newspaper-berliner", 110 | "newspaper-broadsheet", 111 | "presentation-16-9", 112 | "presentation-4-3", 113 | ), 114 | ) 115 | -------------------------------------------------------------------------------- /src/types.typ: -------------------------------------------------------------------------------- 1 | #import "base-type.typ": base-type 2 | #import "assertions.typ": one-of 3 | #import "types/array.typ": array 4 | #import "types/dictionary.typ": dictionary 5 | #import "types/logical.typ": either 6 | #import "types/number.typ": number, integer, floating-point 7 | #import "types/sink.typ": sink 8 | #import "types/string.typ": string, ip, email 9 | #import "types/tuple.typ": tuple 10 | 11 | #let alignment = base-type.with(name: "alignment", types: (alignment,)) 12 | #let angle = base-type.with(name: "angle", types: (angle,)) 13 | #let any = base-type.with(name: "any") 14 | #let boolean = base-type.with(name: "bool", types: (bool,)) 15 | #let bytes = base-type.with(name: "bytes", types: (bytes,)) 16 | #let color = base-type.with(name: "color", types: (color,)) 17 | #let content = base-type.with(name: "content", types: (content, str, symbol)) 18 | #let date = base-type.with(name: "date", types: (datetime,)) 19 | #let direction = base-type.with(name: "direction", types: (direction,)) 20 | #let function = base-type.with(name: "function", types: (function,)) 21 | #let fraction = base-type.with(name: "fraction", types: (fraction,)) 22 | #let gradient = base-type.with(name: "gradient", types: (gradient,)) 23 | #let label = base-type.with(name: "label", types: (label,)) 24 | #let length = base-type.with(name: "length", types: (length,)) 25 | #let location = base-type.with(name: "location", types: (location,)) 26 | #let plugin = base-type.with(name: "plugin", types: (plugin,)) 27 | #let ratio = base-type.with(name: "ratio", types: (ratio,)) 28 | #let regex = base-type.with(name: "regex", types: (regex,)) 29 | #let relative = base-type.with( 30 | name: "relative", 31 | types: (relative, ratio, length), 32 | ) 33 | #let selector = base-type.with(name: "selector", types: (selector,)) 34 | #let stroke = base-type.with(name: "stroke", types: (stroke,)) 35 | #let symbol = base-type.with(name: "symbol", types: (symbol,)) 36 | #let version = base-type.with(name: "version", types: (version,)) 37 | 38 | #let choice(list, assertions: (), ..args) = base-type( 39 | name: "enum", 40 | ..args, 41 | assertions: (one-of(list), ..assertions), 42 | ) + ( 43 | choices: list, 44 | ) 45 | -------------------------------------------------------------------------------- /src/types/array.typ: -------------------------------------------------------------------------------- 1 | #import "../base-type.typ": base-type 2 | #import "../assertions-util.typ": assert-base-type, assert-positive-type 3 | #import "../ctx.typ": z-ctx 4 | 5 | #let array-type = type(()) 6 | 7 | #let array( 8 | name: "array", 9 | assertions: (), 10 | min: none, 11 | max: none, 12 | ..args, 13 | ) = { 14 | let descendents-schema = args.pos().at(0, default: base-type(name: "any")) 15 | 16 | assert-base-type(descendents-schema, scope: ("arguments",)) 17 | assert-positive-type(min, types: (int,), name: "Minimum length") 18 | assert-positive-type(max, types: (int,), name: "Maximum length") 19 | 20 | base-type( 21 | name: name, 22 | description: name + "[" + (descendents-schema.description) + "]", 23 | default: (), 24 | types: (array-type,), 25 | ..args.named(), 26 | ) + ( 27 | min: min, 28 | max: max, 29 | assertions: ( 30 | ( 31 | precondition: "min", 32 | condition: (self, it) => it.len() >= self.min, 33 | message: (self, it) => "Length must be at least " + str(self.min), 34 | ), 35 | ( 36 | precondition: "max", 37 | condition: (self, it) => it.len() <= self.max, 38 | message: (self, it) => "Length must be at most " + str(self.max), 39 | ), 40 | ..assertions, 41 | ), 42 | descendents-schema: descendents-schema, 43 | handle-descendents: (self, it, ctx: z-ctx(), scope: ()) => { 44 | for (key, value) in it.enumerate() { 45 | it.at(key) = (descendents-schema.validate)( 46 | descendents-schema, 47 | value, 48 | ctx: ctx, 49 | scope: (..scope, str(key)), 50 | ) 51 | } 52 | it 53 | }, 54 | ) 55 | } -------------------------------------------------------------------------------- /src/types/dictionary.typ: -------------------------------------------------------------------------------- 1 | #import "../base-type.typ": base-type 2 | #import "../assertions-util.typ": assert-base-type-dictionary, assert-base-type 3 | #import "../ctx.typ": z-ctx 4 | #import "../assertions-util.typ": * 5 | 6 | #let dictionary-type = type((:)) 7 | 8 | /// Valkyrie schema generator for dictionary types. Named arguments define validation schema for entries. Dictionaries can be nested. 9 | /// 10 | /// -> schema 11 | #let dictionary( 12 | dictionary-schema, 13 | name: "dictionary", 14 | default: (:), 15 | pre-transform: (self, it) => it, 16 | aliases: (:), 17 | ..args, 18 | ) = { 19 | 20 | assert-base-type-dictionary(dictionary-schema) 21 | 22 | base-type( 23 | name: name, 24 | default: default, 25 | types: (dictionary-type,), 26 | pre-transform: (self, it) => { 27 | it = pre-transform(self, it) 28 | for (src, dst) in aliases { 29 | let value = it.at(src, default: none) 30 | if (value != none) { 31 | it.insert(dst, value) 32 | let _ = it.remove(src) 33 | } 34 | } 35 | return it 36 | }, 37 | ..args.named() 38 | ) + ( 39 | dictionary-schema: dictionary-schema, 40 | handle-descendents: (self, it, ctx: z-ctx(), scope: ()) => { 41 | 42 | if (it.len() == 0 and self.optional) { 43 | return none 44 | } 45 | 46 | // Check `it` if strict 47 | if (ctx.strict == true) { 48 | for (key, value) in it { 49 | if (key not in self.dictionary-schema) { 50 | return (self.fail-validation)( 51 | self, 52 | it, 53 | ctx: ctx, 54 | scope: scope, 55 | message: "Unknown key `" + key + "` in dictionary", 56 | ) 57 | } 58 | } 59 | } 60 | 61 | 62 | for (key, schema) in self.dictionary-schema { 63 | 64 | let entry = ( 65 | schema.validate 66 | )( 67 | schema, 68 | it.at(key, default: none), // implicitly handles missing entries 69 | ctx: ctx, 70 | scope: (..scope, str(key)) 71 | ) 72 | 73 | if (entry != none or ctx.remove-optional-none == false) { 74 | it.insert(key, entry) 75 | } 76 | 77 | } 78 | return it 79 | }, 80 | ) 81 | } -------------------------------------------------------------------------------- /src/types/logical.typ: -------------------------------------------------------------------------------- 1 | #import "../base-type.typ": base-type 2 | #import "../assertions-util.typ": assert-base-type-array 3 | #import "../ctx.typ": z-ctx 4 | 5 | /// Valkyrie schema generator for objects that can be any of multiple types. 6 | /// 7 | /// -> schema 8 | #let either( 9 | strict: false, 10 | ..args, 11 | ) = { 12 | 13 | assert( 14 | args.pos().len() > 0, 15 | message: "z.either requires 1 or more arguments.", 16 | ) 17 | assert-base-type-array(args.pos(), scope: ("arguments",)) 18 | 19 | base-type( 20 | name: "either", 21 | description: "[" + args.pos().map(it => it.name).join(", ", last: " or ") + "]", 22 | ..args.named(), 23 | ) + ( 24 | strict: strict, 25 | options: args.pos(), 26 | handle-descendents: (self, it, ctx: z-ctx(), scope: ()) => { 27 | for option in self.options { 28 | let ret = (option.validate)( 29 | option, 30 | it, 31 | ctx: z-ctx(ctx, strict: self.strict, soft-error: true), 32 | scope: scope, 33 | ) 34 | if ret != none { 35 | return ret 36 | } 37 | } 38 | 39 | let message = ( 40 | "Type failed to match any of possible options: " + self.options.map(it => it.description).join( 41 | ", ", 42 | last: " or ", 43 | ) + ". Got " + type(it) 44 | ) 45 | 46 | return (self.fail-validation)( 47 | self, 48 | it, 49 | ctx: ctx, 50 | scope: scope, 51 | message: message, 52 | ) 53 | }, 54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /src/types/number.typ: -------------------------------------------------------------------------------- 1 | #import "../base-type.typ": base-type 2 | #import "../assertions-util.typ": * 3 | 4 | #let number( 5 | assertions: (), 6 | min: none, 7 | max: none, 8 | ..args, 9 | ) = { 10 | 11 | assert-positive-type(min, types: (int,), name: "Minimum length") 12 | assert-positive-type(max, types: (int,), name: "Maximum length") 13 | 14 | base-type(name: "number", types: (float, int), ..args) + ( 15 | min: min, 16 | max: max, 17 | assertions: ( 18 | ( 19 | precondition: "min", 20 | condition: (self, it) => it >= self.min, 21 | message: (self, it) => "Value must be at least " + str(self.min), 22 | ), 23 | ( 24 | precondition: "max", 25 | condition: (self, it) => it <= self.max, 26 | message: (self, it) => "Value must be at most " + str(self.max), 27 | ), 28 | ..assertions, 29 | ), 30 | ) 31 | } 32 | 33 | #let integer = number.with(description: "integer", types: (int,)) 34 | #let floating-point = number.with(description: "float", types: (float,)) -------------------------------------------------------------------------------- /src/types/sink.typ: -------------------------------------------------------------------------------- 1 | #import "../base-type.typ": base-type 2 | #import "../assertions-util.typ": assert-base-type 3 | #import "../ctx.typ": z-ctx 4 | 5 | #let to-args-type(..args) = args 6 | 7 | #let sink( 8 | positional: none, 9 | named: none, 10 | ..args, 11 | ) = { 12 | 13 | if positional != none { 14 | assert-base-type(positional) 15 | } 16 | if named != none { 17 | assert-base-type(named) 18 | } 19 | 20 | base-type( 21 | name: "argument-sink", 22 | types: (arguments,), 23 | ..args, 24 | ) + ( 25 | positional-schema: positional, 26 | named-schema: named, 27 | handle-descendents: (self, it, ctx: z-ctx(), scope: ()) => { 28 | 29 | let positional = it.pos() 30 | if self.positional-schema == none { 31 | if positional.len() > 0 { 32 | (self.fail-validation)( 33 | self, 34 | it, 35 | scope: scope, 36 | ctx: ctx, 37 | message: "Unexpected positional arguments.", 38 | ) 39 | } 40 | } else { 41 | positional = (self.positional-schema.validate)( 42 | self.positional-schema, 43 | it.pos(), 44 | ctx: ctx, 45 | scope: (..scope, "positional"), 46 | ) 47 | } 48 | 49 | let named = it.named() 50 | if self.named-schema == none { 51 | if named.len() > 0 { 52 | (self.fail-validation)( 53 | self, 54 | it, 55 | scope: scope, 56 | ctx: ctx, 57 | message: "Unexpected named arguments.", 58 | ) 59 | } 60 | } else { 61 | named = (self.named-schema.validate)( 62 | self.named-schema, 63 | it.named(), 64 | ctx: ctx, 65 | scope: (..scope, "named"), 66 | ) 67 | } 68 | 69 | to-args-type(..positional, ..named) 70 | }, 71 | ) 72 | 73 | } -------------------------------------------------------------------------------- /src/types/string.typ: -------------------------------------------------------------------------------- 1 | #import "../base-type.typ": base-type 2 | #import "../assertions-util.typ": assert-base-type 3 | #import "../ctx.typ": z-ctx 4 | #import "../assertions-util.typ": * 5 | #import "../assertions/string.typ": matches 6 | 7 | /// Valkyrie schema generator for strings 8 | /// 9 | /// -> schema 10 | #let string( 11 | assertions: (), 12 | min: none, 13 | max: none, 14 | ..args, 15 | ) = { 16 | 17 | assert-positive-type(min, types: (int,), name: "Minimum length") 18 | assert-positive-type(max, types: (int,), name: "Maximum length") 19 | 20 | base-type(name: "string", types: (str,), ..args) + ( 21 | min: min, 22 | max: max, 23 | assertions: ( 24 | ( 25 | precondition: "min", 26 | condition: (self, it) => it.len() >= self.min, 27 | message: (self, it) => "Length must be at least " + str(self.min), 28 | ), 29 | ( 30 | precondition: "max", 31 | condition: (self, it) => it.len() <= self.max, 32 | message: (self, it) => "Length must be at most " + str(self.max), 33 | ), 34 | ..assertions, 35 | ), 36 | ) 37 | } 38 | 39 | #let email = string.with( 40 | description: "email", 41 | assertions: ( 42 | matches( 43 | regex("^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]{2,3}){1,2}$"), 44 | message: (self, it) => "Must be an email address", 45 | ), 46 | ), 47 | ); 48 | 49 | #let ip = string.with( 50 | description: "ip", 51 | assertions: ( 52 | matches( 53 | regex("^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$"), 54 | message: (self, it) => "Must be a valid IP address", 55 | ), 56 | ), 57 | ); 58 | -------------------------------------------------------------------------------- /src/types/tuple.typ: -------------------------------------------------------------------------------- 1 | #import "../base-type.typ": base-type 2 | #import "../assertions-util.typ": assert-base-type-array 3 | #import "../ctx.typ": z-ctx 4 | #import "../assertions-util.typ": * 5 | 6 | /// Valkyrie schema generator for an array type with positional type reqruiements. If all entries 7 | /// have the same type, see @@array. 8 | /// exact (bool): Requires a tuple to match in length 9 | /// 10 | /// -> schema 11 | #let tuple( 12 | exact: true, 13 | ..args, 14 | ) = { 15 | assert-base-type-array(args.pos()) 16 | 17 | base-type( 18 | name: "tuple", 19 | types: (type(()),), 20 | ..args.named(), 21 | ) + ( 22 | tuple-exact: exact, 23 | tuple-schema: args.pos(), 24 | handle-descendents: (self, it, ctx: z-ctx(), scope: ()) => { 25 | if (self.tuple-exact and self.tuple-schema.len() != it.len()){ 26 | (self.fail-validation)(self, it, ctx: ctx, scope: scope, 27 | message: "Expected " + str(self.tuple-schema.len()) + " values, but got " + str(it.len()) 28 | ) 29 | } 30 | for (key, schema) in self.tuple-schema.enumerate() { 31 | it.at(key) = (schema.validate)( 32 | schema, 33 | it.at(key), 34 | ctx: ctx, 35 | scope: (..scope, str(key)), 36 | ) 37 | } 38 | it 39 | }, 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | # added by typst-test 2 | **/out/ 3 | **/diff/ 4 | -------------------------------------------------------------------------------- /tests/.ignore: -------------------------------------------------------------------------------- 1 | # added by typst-test 2 | **.png 3 | **.svg 4 | **.pdf 5 | -------------------------------------------------------------------------------- /tests/assertions/comparative/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst-community/valkyrie/8b47671beb30d08bd35c3c702268e17c89622dec/tests/assertions/comparative/ref/1.png -------------------------------------------------------------------------------- /tests/assertions/comparative/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/lib.typ" as z 2 | #import "/tests/utility.typ": * 3 | 4 | #show: show-rule.with(); 5 | 6 | #let soft-parse = z.parse.with(ctx: z.z-ctx(soft-error: true)) 7 | 8 | = Assertions/Comparative 9 | == z.assert.min 10 | 11 | #let min-number-schema(val) = z.number(assertions: (z.assert.min(val),)) 12 | 13 | #{ 14 | soft-parse(5, min-number-schema(4)) == 5 15 | }\ 16 | #{ 17 | soft-parse(5, min-number-schema(5)) == 5 18 | }\ 19 | #{ 20 | soft-parse(5, min-number-schema(6)) == none 21 | } 22 | 23 | == z.assert.max 24 | 25 | #let max-number-schema(val) = z.number(assertions: (z.assert.max(val),)) 26 | 27 | #{ 28 | soft-parse(5, max-number-schema(4)) == none 29 | }\ 30 | #{ 31 | soft-parse(5, max-number-schema(5)) == 5 32 | }\ 33 | #{ 34 | soft-parse(5, max-number-schema(6)) == 5 35 | } 36 | 37 | == z.assert.eq 38 | 39 | #let eq-number-schema(val) = z.number(assertions: (z.assert.eq(val),)) 40 | 41 | #{ 42 | soft-parse(5, eq-number-schema(4)) == none 43 | }\ 44 | #{ 45 | soft-parse(5, eq-number-schema(5)) == 5 46 | }\ 47 | #{ 48 | soft-parse(5, eq-number-schema(6)) == none 49 | } -------------------------------------------------------------------------------- /tests/contexts/remove-optional-none/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst-community/valkyrie/8b47671beb30d08bd35c3c702268e17c89622dec/tests/contexts/remove-optional-none/ref/1.png -------------------------------------------------------------------------------- /tests/contexts/remove-optional-none/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/lib.typ" as z 2 | #set page(height: auto, width: auto) 3 | 4 | #let test-schema = z.dictionary(( 5 | id: z.string(optional: true), 6 | index: z.string(optional: true), 7 | name: z.string(), 8 | institution: z.string(optional: true), 9 | )) 10 | 11 | #let test-dictionary = ( 12 | name: "Helsdflo", 13 | id: none, 14 | ) 15 | 16 | #z.parse( 17 | test-dictionary, 18 | test-schema, 19 | ctx: z.z-ctx(remove-optional-none: false), 20 | ) 21 | 22 | #z.parse(test-dictionary, test-schema, ctx: z.z-ctx(remove-optional-none: true)) 23 | -------------------------------------------------------------------------------- /tests/contexts/strict/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst-community/valkyrie/8b47671beb30d08bd35c3c702268e17c89622dec/tests/contexts/strict/ref/1.png -------------------------------------------------------------------------------- /tests/contexts/strict/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/lib.typ" as z 2 | #set page(height: 1cm, width: 1cm) 3 | 4 | #{ 5 | 6 | let strict-context = z.z-ctx(strict: true) 7 | 8 | let test-dictionary = ( 9 | //string: "world", 10 | // number: 1.2, 11 | email: "hello@world.com", 12 | ip: "1.1.251.1", 13 | ) 14 | 15 | _ = z.parse( 16 | test-dictionary, 17 | z.dictionary(( 18 | email: z.email(), 19 | ip: z.ip(), 20 | )), 21 | ctx: strict-context, 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /tests/logical/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst-community/valkyrie/8b47671beb30d08bd35c3c702268e17c89622dec/tests/logical/ref/1.png -------------------------------------------------------------------------------- /tests/logical/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/lib.typ" as z 2 | #import "/tests/utility.typ": * 3 | 4 | #show: show-rule.with(); 5 | 6 | = logical/either 7 | == Input types 8 | #{ 9 | let schema = z.either(z.email(), z.ip()) 10 | let input-types = ( 11 | "ip (1.1.1.1)": "1.1.1.1", 12 | "email": "test@hello.wor.ld", 13 | ) 14 | 15 | for (name, value) in input-types { 16 | utility-expect-eq( 17 | test: value, 18 | schema: schema, 19 | truth: value, 20 | )([It should validate #name]) 21 | } 22 | } 23 | 24 | #{ 25 | let schema = z.either( 26 | strict: true, 27 | z.dictionary(( 28 | seed: z.integer(), 29 | )), 30 | z.dictionary(( 31 | dynamic: z.boolean(), 32 | )), 33 | ) 34 | 35 | z.parse( 36 | (dynamic: false), 37 | schema, 38 | ) 39 | } -------------------------------------------------------------------------------- /tests/schemas/author/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst-community/valkyrie/8b47671beb30d08bd35c3c702268e17c89622dec/tests/schemas/author/ref/1.png -------------------------------------------------------------------------------- /tests/schemas/author/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/lib.typ" as z 2 | #import "/tests/utility.typ": * 3 | 4 | #show: show-rule.with(); 5 | 6 | = Integration 7 | 8 | == Author schema 9 | 10 | #[ 11 | #z.parse( 12 | ( 13 | name: "James R Swift", 14 | email: "hello@world.com", 15 | // test: true, 16 | ), 17 | z.schemas.author, 18 | ctx: z.z-ctx(strict: true), 19 | ) 20 | ] 21 | -------------------------------------------------------------------------------- /tests/types/any/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst-community/valkyrie/8b47671beb30d08bd35c3c702268e17c89622dec/tests/types/any/ref/1.png -------------------------------------------------------------------------------- /tests/types/any/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/lib.typ" as z 2 | #import "/tests/utility.typ": * 3 | 4 | #show: show-rule.with(); 5 | 6 | #let schema = z.any() 7 | 8 | = types/any 9 | == Input types 10 | #{ 11 | let input-types = ( 12 | "content (empty)": [], 13 | "content (lorem)": lorem(20), 14 | "number (0)": 0, 15 | "number (negative float)": -1.2, 16 | "string (empty)": "", 17 | "string (lorem)": str(lorem(20)), 18 | "dictionary (empty)": (:), 19 | "dictionary": (foo: "bar"), 20 | ) 21 | 22 | for (name, value) in input-types { 23 | utility-expect-eq( 24 | test: value, 25 | schema: schema, 26 | truth: value, 27 | )([It should validate #name]) 28 | } 29 | } 30 | 31 | #z.parse(none, z.any(optional: true)) -------------------------------------------------------------------------------- /tests/types/array/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst-community/valkyrie/8b47671beb30d08bd35c3c702268e17c89622dec/tests/types/array/ref/1.png -------------------------------------------------------------------------------- /tests/types/array/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/lib.typ" as z 2 | #import "/tests/utility.typ": * 3 | 4 | #show: show-rule.with(); 5 | 6 | #let schema = z.array() 7 | 8 | = types/array 9 | == Input types 10 | #{ 11 | let input-types = ( 12 | "array (empty)": (), 13 | "array (single)": (0,), 14 | ) 15 | 16 | for (name, value) in input-types { 17 | utility-expect-eq( 18 | test: value, 19 | schema: schema, 20 | truth: value, 21 | )([It should validate #name]) 22 | } 23 | } 24 | 25 | == Test \#1 26 | #let test-array = ("me@tinger.dev", "hello@world.ac.uk") 27 | #test-array 28 | 29 | #utility-expect-eq( 30 | test: test-array, 31 | schema: z.array(), 32 | truth: test-array, 33 | )([Test satisfies array\]) 34 | 35 | #utility-expect-eq( 36 | test: test-array, 37 | schema: z.array(z.email()), 38 | truth: test-array, 39 | )([Test satisfies array\]) 40 | 41 | === Assertions - Minimum length 42 | 43 | #utility-expect-eq( 44 | test: test-array, 45 | schema: z.array(z.email(), min: 1), 46 | truth: test-array, 47 | )([Test satisfies array\ min length 1]) 48 | 49 | #utility-expect-eq( 50 | test: test-array, 51 | schema: z.array(z.email(), min: 2), 52 | truth: test-array, 53 | )([Test satisfies array\ min length 2]) 54 | 55 | #utility-expect-eq( 56 | test: test-array, 57 | schema: z.array(z.email(), min: 3), 58 | truth: (), 59 | )([Test fails array\ min length 3]) 60 | 61 | === Assertions - Maximum length 62 | 63 | #utility-expect-eq( 64 | test: test-array, 65 | schema: z.array(z.email(), max: 1), 66 | truth: (), 67 | )([Test fails array\ max length 1]) 68 | 69 | #utility-expect-eq( 70 | test: test-array, 71 | schema: z.array(z.email(), max: 2), 72 | truth: test-array, 73 | )([Test satisfies array\ max length 2]) 74 | 75 | #utility-expect-eq( 76 | test: test-array, 77 | schema: z.array(z.email(), max: 3), 78 | truth: test-array, 79 | )([Test satisfies array\ max length 3]) 80 | 81 | === Assertions - Exact length 82 | 83 | #utility-expect-eq( 84 | test: test-array, 85 | schema: z.array(z.email(), assertions: (z.assert.length.equals(2),)), 86 | truth: test-array, 87 | )([Test satisfies array\ equal length 2]) 88 | 89 | #utility-expect-eq( 90 | test: test-array, 91 | schema: z.array(z.email(), assertions: (z.assert.length.equals(3),)), 92 | truth: (), 93 | )([Test fails array\ equal length 3]) -------------------------------------------------------------------------------- /tests/types/boolean/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst-community/valkyrie/8b47671beb30d08bd35c3c702268e17c89622dec/tests/types/boolean/ref/1.png -------------------------------------------------------------------------------- /tests/types/boolean/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/lib.typ" as z 2 | #import "/tests/utility.typ": * 3 | 4 | #show: show-rule.with(); 5 | #let schema = z.boolean() 6 | 7 | = types/boolean 8 | == Input types 9 | #{ 10 | let input-types = ( 11 | "boolean (true)": true, 12 | "boolean (false)": false, 13 | ) 14 | 15 | for (name, value) in input-types { 16 | utility-expect-eq( 17 | test: value, 18 | schema: schema, 19 | truth: value, 20 | )([It should validate #name]) 21 | } 22 | } 23 | 24 | #{ 25 | let input-types = ( 26 | "none": none, 27 | "number (0)": 0, 28 | ) 29 | 30 | for (name, value) in input-types { 31 | utility-expect-eq( 32 | test: value, 33 | schema: schema, 34 | truth: none, 35 | )([It should fail #name]) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/types/choice/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst-community/valkyrie/8b47671beb30d08bd35c3c702268e17c89622dec/tests/types/choice/ref/1.png -------------------------------------------------------------------------------- /tests/types/choice/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/lib.typ" as z 2 | #import "/tests/utility.typ": * 3 | 4 | #show: show-rule.with(); 5 | #let schema = z.choice(("a", "b", 1)) 6 | 7 | = types/choice 8 | == Input types 9 | #{ 10 | let input-types = ( 11 | "choice (a)": "a", 12 | "choice (b)": "b", 13 | "choice (1)": 1, 14 | ) 15 | 16 | for (name, value) in input-types { 17 | utility-expect-eq( 18 | test: value, 19 | schema: schema, 20 | truth: value, 21 | )([It should validate #name]) 22 | } 23 | } 24 | 25 | #{ 26 | let input-types = ( 27 | "none": none, 28 | "number (0)": 0, 29 | ) 30 | 31 | for (name, value) in input-types { 32 | utility-expect-eq( 33 | test: value, 34 | schema: schema, 35 | truth: none, 36 | )([It should fail #name]) 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /tests/types/color/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst-community/valkyrie/8b47671beb30d08bd35c3c702268e17c89622dec/tests/types/color/ref/1.png -------------------------------------------------------------------------------- /tests/types/color/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/lib.typ" as z 2 | #set page(height: 1cm, width: 1cm) 3 | 4 | #{ 5 | _ = z.parse(rgb(0, 0, 0), z.color()) 6 | _ = z.parse(cmyk(0%, 0%, 0%, 0%), z.color()) 7 | //_ = z.parse(0, z.color()) 8 | //_ = z.parse(none, z.color()) 9 | } 10 | -------------------------------------------------------------------------------- /tests/types/content/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst-community/valkyrie/8b47671beb30d08bd35c3c702268e17c89622dec/tests/types/content/ref/1.png -------------------------------------------------------------------------------- /tests/types/content/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/lib.typ" as z 2 | #set page(height: 1cm, width: 1cm) 3 | 4 | #{ 5 | _ = z.parse([123465], z.content()) 6 | } 7 | -------------------------------------------------------------------------------- /tests/types/datetime/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst-community/valkyrie/8b47671beb30d08bd35c3c702268e17c89622dec/tests/types/datetime/ref/1.png -------------------------------------------------------------------------------- /tests/types/datetime/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/lib.typ" as z 2 | #import "/tests/utility.typ": * 3 | 4 | #show: show-rule.with(); 5 | #let schema = z.date() 6 | 7 | = types/boolean 8 | == Input types 9 | #{ 10 | let input-types = ( 11 | "date (true)": datetime.today(), 12 | ) 13 | 14 | for (name, value) in input-types { 15 | utility-expect-eq( 16 | test: value, 17 | schema: schema, 18 | truth: value, 19 | )([It should validate #name]) 20 | } 21 | } 22 | 23 | #{ 24 | let input-types = ( 25 | "none": none, 26 | "number (0)": 0, 27 | ) 28 | 29 | for (name, value) in input-types { 30 | utility-expect-eq( 31 | test: value, 32 | schema: schema, 33 | truth: none, 34 | )([It should fail #name]) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/types/dictionary/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst-community/valkyrie/8b47671beb30d08bd35c3c702268e17c89622dec/tests/types/dictionary/ref/1.png -------------------------------------------------------------------------------- /tests/types/dictionary/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/lib.typ" as z 2 | 3 | #let test-dictionary = ( 4 | string: "world", 5 | number: 1.2, 6 | email: "hello@world.com", 7 | ip: "1.1.251.1", 8 | ) 9 | 10 | #z.parse( 11 | test-dictionary, 12 | z.dictionary(( 13 | string: z.string(assertions: (z.assert.length.min(5),), optional: true), 14 | number: z.number(optional: true), 15 | email: z.email(optional: true), 16 | ip: z.ip(optional: true), 17 | )), 18 | ) 19 | -------------------------------------------------------------------------------- /tests/types/gradient/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst-community/valkyrie/8b47671beb30d08bd35c3c702268e17c89622dec/tests/types/gradient/ref/1.png -------------------------------------------------------------------------------- /tests/types/gradient/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/lib.typ" as z 2 | #import "/tests/utility.typ": * 3 | 4 | #show: show-rule.with(); 5 | 6 | #let schema = z.gradient() 7 | 8 | = types/gradient 9 | == Input types 10 | #let _ = z.parse(gradient.linear(..color.map.rainbow), schema) -------------------------------------------------------------------------------- /tests/types/number/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst-community/valkyrie/8b47671beb30d08bd35c3c702268e17c89622dec/tests/types/number/ref/1.png -------------------------------------------------------------------------------- /tests/types/number/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/lib.typ" as z 2 | #import "/tests/utility.typ": * 3 | 4 | #show: show-rule.with(); 5 | 6 | #let schema = z.any() 7 | 8 | = types/number 9 | == Input types 10 | 11 | == Custom assertions 12 | 13 | === Min 14 | #let test-min(valid: true, value, minimum: 0) = utility-expect-eq( 15 | test: value, 16 | schema: z.number(min: minimum), 17 | truth: if (valid) { 18 | value 19 | } else { 20 | none 21 | }, 22 | )([Comparing #value against minimum #minimum]) 23 | 24 | #test-min(valid: true, 5, minimum: 4) 25 | #test-min(valid: true, 5, minimum: 5) 26 | #test-min(valid: false, 5, minimum: 6) 27 | 28 | === Min 29 | #let test-max(valid: true, value, maximum: 0) = utility-expect-eq( 30 | test: value, 31 | schema: z.number(max: maximum), 32 | truth: if (valid) { 33 | value 34 | } else { 35 | none 36 | }, 37 | )([Comparing #value against maximum #maximum]) 38 | 39 | #test-max(valid: false, 5, maximum: 4) 40 | #test-max(valid: true, 5, maximum: 5) 41 | #test-max(valid: true, 5, maximum: 6) -------------------------------------------------------------------------------- /tests/types/sink/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst-community/valkyrie/8b47671beb30d08bd35c3c702268e17c89622dec/tests/types/sink/ref/1.png -------------------------------------------------------------------------------- /tests/types/sink/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/lib.typ" as z 2 | #import "/tests/utility.typ": * 3 | 4 | #show: show-rule.with(); 5 | 6 | #let positional-schema = z.array() 7 | #let named-schema = z.dictionary((named: z.string())) 8 | #let sink-schema = z.sink( 9 | positional: positional-schema, 10 | named: named-schema, 11 | ) 12 | 13 | #let to-args-type(..args) = args 14 | = types/sink 15 | 16 | #{ 17 | let _ = z.parse(to-args-type("hello", named: "0"), sink-schema) 18 | } 19 | // #{let _ = z.parse(to-args-type("hello"), sink-schema)} 20 | #{ 21 | let _ = z.parse(to-args-type(named: "0"), sink-schema) 22 | } 23 | // #{ z.parse(to-args-type(), sink-schema)} -------------------------------------------------------------------------------- /tests/types/string/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst-community/valkyrie/8b47671beb30d08bd35c3c702268e17c89622dec/tests/types/string/ref/1.png -------------------------------------------------------------------------------- /tests/types/string/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/lib.typ" as z 2 | #import "/tests/utility.typ": * 3 | 4 | #show: show-rule.with(); 5 | 6 | #let schema = z.any() 7 | 8 | = types/number 9 | == Input types 10 | 11 | == Custom assertions 12 | 13 | === Min 14 | #let test-min(valid: true, value, minimum: 0) = utility-expect-eq( 15 | test: value, 16 | schema: z.string(min: minimum), 17 | truth: if (valid) { 18 | value 19 | } else { 20 | none 21 | }, 22 | )([Comparing #value against minimum #minimum]) 23 | 24 | #test-min(valid: false, "", minimum: 2) 25 | #test-min(valid: false, "a", minimum: 2) 26 | #test-min(valid: true, "ab", minimum: 2) 27 | #test-min(valid: true, "abc", minimum: 2) 28 | 29 | === Min 30 | #let test-min(valid: true, value, maximum: 0) = utility-expect-eq( 31 | test: value, 32 | schema: z.string(max: maximum), 33 | truth: if (valid) { 34 | value 35 | } else { 36 | none 37 | }, 38 | )([Comparing #value against minimum #maximum]) 39 | 40 | #test-min(valid: true, "", maximum: 1) 41 | #test-min(valid: true, "a", maximum: 1) 42 | #test-min(valid: false, "ab", maximum: 1) 43 | #test-min(valid: false, "abc", maximum: 1) 44 | 45 | == Specializations 46 | #z.parse("hello@world.co.uk", z.email()) 47 | #z.parse("192.168.0.1", z.ip()) 48 | 49 | == default 50 | #let _ = z.parse(none, z.string(default: "Hello")) 51 | #let _ = z.parse(auto, z.string(default: "Hello")) 52 | #let _ = z.parse(auto, z.string(default: "Hello", optional: true)) 53 | #let _ = repr(z.parse(auto, z.string(optional: true))) 54 | // #z.parse(auto, z.string()) \ 55 | #let _ = z.parse("none", z.string(default: "Hello")) -------------------------------------------------------------------------------- /tests/types/stroke/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst-community/valkyrie/8b47671beb30d08bd35c3c702268e17c89622dec/tests/types/stroke/ref/1.png -------------------------------------------------------------------------------- /tests/types/stroke/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/lib.typ" as z 2 | #import "/tests/utility.typ": * 3 | 4 | #show: show-rule.with(); 5 | 6 | #let schema = z.stroke() 7 | 8 | = types/stroke 9 | == Input types 10 | #let _ = z.parse(stroke(), schema) -------------------------------------------------------------------------------- /tests/types/tuple/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst-community/valkyrie/8b47671beb30d08bd35c3c702268e17c89622dec/tests/types/tuple/ref/1.png -------------------------------------------------------------------------------- /tests/types/tuple/ref/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst-community/valkyrie/8b47671beb30d08bd35c3c702268e17c89622dec/tests/types/tuple/ref/2.png -------------------------------------------------------------------------------- /tests/types/tuple/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/lib.typ" as z 2 | #set page(height: 1cm, width: 1cm) 3 | 4 | #{ 5 | let test-tuple = ( 6 | "123", 7 | "email@address.co.uk", 8 | 1.1 9 | ) 10 | 11 | z.parse( 12 | test-tuple, 13 | z.tuple( 14 | z.string(), 15 | z.email(), 16 | z.floating-point(), 17 | ), 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /tests/types/version/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst-community/valkyrie/8b47671beb30d08bd35c3c702268e17c89622dec/tests/types/version/ref/1.png -------------------------------------------------------------------------------- /tests/types/version/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/lib.typ" as z 2 | #import "/tests/utility.typ": * 3 | 4 | #show: show-rule.with(); 5 | 6 | #let schema = z.version() 7 | 8 | = types/version 9 | == Input types 10 | #let _ = z.parse(version(0, 1, 0), schema) -------------------------------------------------------------------------------- /tests/utility.typ: -------------------------------------------------------------------------------- 1 | #import "/src/lib.typ" as z 2 | 3 | #let show-rule(body, ..args) = { 4 | set page(height: auto) 5 | body 6 | } 7 | 8 | #let utility-expect-eq( 9 | schema: none, 10 | test: none, 11 | truth: none, 12 | ) = block.with( 13 | width: 100%, 14 | inset: 8pt, 15 | radius: 4pt, 16 | fill: if (z.parse(test, schema, ctx: z.z-ctx(soft-error: true)) == truth) { 17 | rgb("#c4e4bd") 18 | } else { 19 | rgb("#f5d3d6") 20 | }, 21 | ) -------------------------------------------------------------------------------- /typst.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "valkyrie" 3 | version = "0.2.2" 4 | entrypoint = "src/lib.typ" 5 | authors = ["James R. Swift", "tinger "] 6 | license = "MIT" 7 | description = "Type safe type validation" 8 | repository = "https://github.com/typst-community/valkyrie" 9 | keywords = ["type", "validation", "zod"] 10 | categories = ["scripting", "utility"] 11 | exclude = [ 12 | ".github", 13 | "docs", 14 | "scripts", 15 | "tests", 16 | ".typstignore", 17 | "Justfile", 18 | "thumbnails", 19 | ] 20 | --------------------------------------------------------------------------------