diff --git a/SCHEMA-SPEC.md b/SCHEMA-SPEC.md index 907c1af..d453edd 100644 --- a/SCHEMA-SPEC.md +++ b/SCHEMA-SPEC.md @@ -6,7 +6,7 @@ constrain the allowed semantics of a KDL document. This can be used for many purposes: documentation for users, automated verification, or even automated generation of bindings! -This document describes KDL Schema version `1.0.0`. It was released on September 11, 2021. +This document describes KDL Schema version `2.0.0`. It is unreleased. ## The Formal Schema @@ -39,6 +39,14 @@ None. * `tag-names` (optional): [Validations](#validation-nodes) to apply to the _names_ of tags of child nodes. * `other-tags-allowed` (optional): Whether to allow node tags other than the ones explicitly listed here. Defaults to `#false`. +#### Example + +```kdl +document { + +} +``` + ### `info` node The `info` node describes the schema itself. diff --git a/schema/cargo.kdl b/schema/cargo.kdl new file mode 100644 index 0000000..f5497ee --- /dev/null +++ b/schema/cargo.kdl @@ -0,0 +1,172 @@ +@kdl:schema "https://github.com/kdl-org/kdl/blob/main/schema/kdl-schema.kdl" + +metadata { + // TODO: update this link when we're ready to release something. + link "https://github.com/kdl-org/kdl/blob/main/schema/cargo.kdl" rel=self + title "Cargo Schema" lang=en + description "KDL-based translation of the Cargo.toml schema." lang=en + author "Kat Marchán" { + link "https://github.com/zkat" rel=self + } + link "https://github.com/kdl-org/kdl" rel=documentation + link "https://doc.rust-lang.org/cargo/reference/manifest.html" rel=documentation + license "Creative Commons Attribution-ShareAlike 4.0 International License" spdx=CC-BY-SA-4.0 { + link "https://creativecommons.org/licenses/by-sa/4.0/" lang=en + } +} + +children { + node package title="Describes a package" { + children { + node name title="The name of the package" { + required + arg { + type string + pattern #"^[a-zA-Z0-0\-_]+$"# + } + } + node version title="The version of the package." { + arg { + type string + // From https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string + pattern #"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"# + } + } + node authors title="The authors of the package." { + repeatable + args { + distinct + type string + } + children { + node - { + repeatable + arg title="Name" { + type string + } + prop email title="Email address" { + type string + format email + } + prop about title="Brief note about author (role, etc)" { + type string + } + } + } + } + node edition title="The Rust edition." { + arg { + type string + enum "2015" "2018" "2021" "2024" + } + } + node rust-version title="The minimal supported Rust version." { + arg { + type string + } + } + node description title="A description of the package." { + arg { + type string + } + } + node documentation title="URL of the package documentation." { + arg { + type string + format url + } + } + node readme title="Path to the package’s README file." { + arg { + type string #boolean + } + } + node homepage title="URL of the package homepage." { + arg { + type string + format url + } + } + node repository title="URL of the package source repository." { + arg { + type string + format url + } + } + node license title="The package license." { + arg { + type string + } + } + node license-file title="Path to the text of the license." { + arg { + type string + } + } + node keywords title="Keywords for the package." { + args { + type string + // No pattern because keyword restrictions are only on + // crates.io + } + } + node categories title="Categories of the package." { + args { + type string + // No pattern because category restrictions are only on + // crates.io + } + } + node workspace title="Path to the workspace for the package." { + arg { + type string + } + } + node build title="Path to the package build script." { + arg { + type string boolean + } + } + node links title="Name of the native library the package links with." { + arg { + type string + } + } + node exclude title="Files to exclude when publishing." { + args { + type string + } + } + node include title="Files to include when publishing." { + args { + type string + } + } + node publish title="Can be used to prevent publishing the package." { + // TODO: This is a good example of where we might need smarter + // comstraints ("either a single boolean, or 1+ strings") + args { + type string boolean + } + ] + node metadata title="Extra settings for external tools." { + repeat + args + props { + allow-others + } + } + node default-run title="The default binary to run by cargo run." { + arg { + type string + } + } + node no-autolib title="Disables library auto discovery." + node no-autobins title="Disables binary auto discovery." + node no-autoexamples title="Disables example auto discovery." + node no-autotests title="Disables test auto discovery." + node no-autobenches title="Disables bench auto discovery." + node resolver title="Sets the dependency resolver to use." + } + } +} diff --git a/schema/ksl-schema.kdl b/schema/ksl-schema.kdl new file mode 100644 index 0000000..751e6cf --- /dev/null +++ b/schema/ksl-schema.kdl @@ -0,0 +1,931 @@ +// TODO: +// * examples +// * dependentRequired +// * dependentSchema +// * if-then-else +// * composition (anyOf, allOf, oneOf, not, etc: https://json-schema.org/understanding-json-schema/reference/combining) +// * something like `xsd:group` for the above, instead? +// * requires (a more general-purpose version of followed-by that uses kpath+children?) +// * something to configure semanticTokens with + +// simplify everything? If you want to deprecate an argument or option, you have +// to make a new node, basically. Which seems sensible, tbh. But might be too +// strict. + +@ksl:schema "https://github.com/kdl-org/kdl/blob/main/examples/ksl-schema.kdl" + +metadata { + // TODO: update this link when we're ready to release something. + id "https://github.com/kdl-org/kdl/blob/main/examples/ksl-schema.kdl" + title "KDL Schema" lang=en + description "KDL Schema schema using KDL Schema" lang=en + author "Kat Marchán" { + link "https://github.com/zkat" rel=self + } + contributor "Lars Willighagen" { + link "https://github.com/larsgw" rel=self + } + link "https://github.com/kdl-org/kdl" rel=documentation + license "Creative Commons Attribution-ShareAlike 4.0 International License" spdx=CC-BY-SA-4.0 { + link "https://creativecommons.org/licenses/by-sa/4.0/" lang=en + } + published "2021-08-31" + modified "2021-09-01" +} + +children { + node metadata about=""" + Schema metadata + + Contains metadata about the schema itself. + """ { + required + children { + node id about=""" + Schema identifier + + The unique identifier for this schema. MUST be a valid URL/IRL. Implementations MAY attempt to visit it, but MUST NOT assume it is valid. + """ { + arg { + type string + format url irl + } + } + node title about=""" + Schema title + + The title of the schema or the format it describes. + """ { + ref lang-mixin + arg about="The title text" { + type string + } + } + node description about=""" + Schema description + + A description of the schema or the format it validates, which + may include its purposes, its usage, and even examples. + """ { + ref lang-mixin + arg about="Description text" { + type string + } + } + node author id=metadata-author about=""" + Schema author + + An author for the schema. + """ { + repeatable + arg description="Person name" { + optional + type string + } + prop orcid description="The ORCID of the person" { + type string + pattern #"\d{4}-\d{4}-\d{4}-\d{4}"# + } + children { + node link { + ref metadata-link + } + } + } + node contributor about=""" + Schema contributor + + A contributor to the schema might not be considered an author. + """ { + ref metadata-author + } + node link id=metadata-link about=""" + External link + + Link to an external resource of some sort, such as the schema + itself (`rel=self`) or documentation (`rel=documentation`). + Implementations MAY visit the URL, but MUST NOT assume it is + valid. + """ { + repeatable + arg about="Link URL\n\nA URL that the link points to." { + type string + format url irl + } + // TODO: expand a bit more about what it means for the enum to be `allow-others`. + prop rel about="Link relationship\n\nThe relation between the current entity and the URL." { + required + type string + enum self documentation { + allow-others + } + } + prop lang about="Link language\n\nLanguage of the destination document." { + ref metadata-lang + } + } + node license about=""" + Schema license + + The license(s) that the schema is licensed under. + """ { + repeatable + arg description="Name of the used license" { + type string + } + prop spdx description="An SPDX license identifier" { + type string + // TODO: validation? + } + prop path about="Path to a local license file" { + type string + } + prop url about="URL to an externally-stored license" { + type string + format url url-reference irl irl-reference + } + children { + node link { + ref metadata-link + } + } + } + node published about=""" + Schema publication date + + Date or data+time when the schema was published. + """ { + arg about="Publication date" { + type string + format date date-time + } + } + node modified about=""" + Schema modification date + + When the schema was modified. If used multiple times, the most + recent date will be considered 'latest'. + """ { + repeatable + args about="Modification date" { + type string + format date date-time + } + } + node version about=""" + Schema semver version + + The version number of this version of the schema, in semver + format. + """ { + arg about="Semver version number" { + type string + // https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string. + pattern #"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"# + } + } + } + } + node definitions about=""" + Inert validation definitions + + An optional set of definitions that may be referenced elsewhere in the + schema. They will be inert (that is, not directly apply to the document) + unless referenced by another node inside `document`. + """ { + children { + allow-others + } + } + node document { + ref children-node + default { + node @ksl:schema about=""" + Schema reference + + Reference(s) for the schema(s) describing this document. They + MUST be properly-formatted URLs. Implementations MAY attempt to + visit them, but MUST NOT assume they are valid. + + If multiple URLs are provided, or if multiple `@ksl:schema` + nodes are present, ALL schemas MUST successfully validate in + order for the document to validate, unless `warn` is `#true`. + + In such a case, implementations SHOULD warn that validation has + failed and report which schema failed to pass--they SHOULD + include more details about what the specific failure was, but + MAY simply indicate that certain schema(s) failed to validate. + """ { + ref about-mixin + repeatable + prop warn { + type boolean + default #false + } + args { + min 1 + type string + format url url-reference irl irl-reference + } + } + } + children { + node children id=children-node about=""" + Node children + + Validations and definitions used for all nodes in this scope. + Children are only allowed on nodes (or the toplevel document) if + at least one `children` node is present in their definitions. + """ { + children { + node names about=""" + Child node name validations + + String validations to apply to all node names in this scope. + """ { + ref string-validations + ref about-mixin + repeatable + } + node allow-others about=""" + Allow other children + + If present/`#true`, allows child nodes in this scope + other than the ones explicitly listed and those allowed + by `names`. + """ { + arg { + type boolean + default #true + } + } + node node about=""" + A KDL node + + Declares a KDL node belonging either to the top-level + document or to another `node`'s children. + """ { + ref about-mixin + repeatable + arg about="Node name\n\nThe name of the node." { + type string + } + prop id about="Node identifier\n\nA schema-unique ID/anchor for this node." { + type string + } + children { + node ref about=""" + A reference to a node defined elsewhere. + + Each `ref` child will be interpreted in order of + appearance. Any overlapping definitions will replace + preceding instances, with each subsequent `ref` + replacing any duplicate node components. + + The replacement rules are as follows, and apply recursively: + * node properties MUST by replaced by key. + * node arguments MUST be replaced by order of appearance. + * `prop` definitions MUST be replaced by key (their first argument) + * `arg` definitions MUST be replaced based on _order of + appearance_. That is, the first `arg` in ref `B` till be + merged into the first `arg` in preceding ref `A`. + * For all other components: + * If the definition specified is marked as + `repeatable`, then all definitions using that node + will be concatenated, with later `ref`s + concatenating definitions after the previous `ref`'s + definitions. + * If the definition is NOT marked as `repeatable`, + it will be replaced by subsequent `ref`s. + + Once all `ref` children are resolved, the containing + node's own items will override anything defined by + `ref`s, using the same rules as above (essentially, the + current node is treated as a 'final `ref`'). + + If both an ID argument and a `path` are provided, + the ID will take precedence and, if not found, fall + back to the path. For `id` and `path` children, + precedence is in order of appearance, regardless of + whether the child is an `id` or a `path`. + + If no items resolve into a valid ref, validation + MUST error, unless the ref is configured as + `optional`, in which case validation MAY warn, but + MUST NOT fail. + """ { + repeatable + arg about="ID reference to another node" { + type string + optional + } + prop path about="KPath reference to another node." { + type string + format kpath + } + prop base about=""" + Base schema + + The schema to resolve references against. If not + provided, the base schema SHALL be the one + defined in `metadata > id` for the current + schema. + + Relative schema references SHALL be resolved + against `metadata > id`. + """ { + type string + format url-reference irl-reference + } + children { + node id about="ID reference to another node." { + repeatable + arg { + type string + } + + } + node path about="KPath reference to another node." { + repeatable + arg { + type string + format kpath + } + } + } + } + // TODO: be more precise and clear about merge + override behavior. + node default about=""" + Definition default + + Any children of this node will be merged into the node + definition itself, as if written directly into the node. + Any conflicting definitions will be overriden by + definitions within the rest of the node. + """ { + children { + allow-others + } + } + node undefine about="Undefine a node with this name" { + arg { + optional + type boolean + default #true + } + } + node required about=""" + Node is required + + By default, all declared child nodes are + optional. Including this option will require + that this node always appear in its parent's + children block. + """ { + arg { + type boolean + default #true + } + } + node repeatable about=""" + Node is repeatable + + By default, each node in a `children` block may + only appear once in its scope. When this option + is present, the node will be allowed to have + multiple instances within the same scope. + """ { + prop min about=""" + Minimum node count + + Minimum number of repeated instances of this + node that must appear in the same scope. + """ { + arg { + gte 0 + type integer + } + } + prop max about=""" + Maximum node count + + Maximum numbers of repeated instances of + this node that may appear in the same scope. + """ { + arg { + gte 0 + type integer + } + } + } + node deprecated about=""" + Mark node as deprecated + + When present, this node will be considered a + deprecated part of the API. You may optionally + supply a message, and/or a reference to a node + that should be used instead. + """ { + arg { + optional + type boolean + default #true + } + prop message about=""" + Deprecation message + + A helpful deprecation message that may + explain why the node was deprecated and + other information, such as when the node + will be removed altogether. Users SHOULD use + `by=` and `by-kpath` to specify what node + this will be replaced with instead of + including it in the `message` itself. + """ { + type string + } + prop by about="Deprecated by this node `id`" { + type string + } + prop by-kpath about="Depreceated by this node KPath" { + type string + format kpath + } + } + node annotations about=""" + Node type annotations + + Validations to apply specifically to arbitrary + node type annotation names. + """ { + ref about-mixin + ref string-validations + repeatable + } + node prop about=""" + Node property + + A node property key/value pair. Properties + declared with `prop` are always optional, unless + marked as `required` or included in + `props:required`. + """ { + ref about-mixin + ref value-validations + repeatable + arg about="The property key" { + type string + } + children about="Property-specific validations" { + node required about="Whether this property is required in the node." { + arg { + optional + type boolean + default #true + } + } + } + } + node props about=""" + General property validations + + Validations to apply to all properties of this + node. + """ { + ref about-mixin + ref value-validations + children { + node names about="Validations to apply to all property names." { + ref string-validations + repeatable + } + node min about=""" + Minimum property count + + Minimum number of properties this node + must have. + """ { + arg { + gte 0 + type integer + } + } + node max about=""" + Maximum property count + + Maximum number of properties this node + may have. + """ { + arg { + gte 0 + type integer + } + } + node required about=""" + List of required props + + List of property names that must be + present on the node. Individual `prop` + nodes may specify additional required + properties beyond those specified in + this list. Properties listed here which + already have a `prop` node marked as + `required` are allowed, but are + redundant. + """ { + args { + min 1 + type string + } + } + node allow-others about=""" + Allow other properties + + If present, allows other properties that + don't match this validator. + """ { + arg { + type boolean + default #true + } + } + } + } + node arg about=""" + Defines an individual, ordered argument + + Each nth instance of this node will specify + validations for the corresponding nth instance + of the arg. Every specified `arg` is required, + in the given order, unless marked as `optional`. + """ { + ref about-mixin + ref value-validations + repeatable + children { + node optional about=""" + Argument is not required + + Specified `arg`s are required by + default. + + `optional` only applies to *presence*: + an existing argument in an optional + `arg` \"slot\" that fails validation + will fail normally, even though it is + optional. As such, `optional` is only + really useful if it is on the last + `arg`, or is only followed by optional + `arg`s. + """ { + arg { + type boolean + default #true + } + } + } + } + // TODO: add a feature that will let us specify that `args` + // MUST be after any existing `arg` nodes in the current + // scope. i.e. you can't do `node x { args; arg }` + node args about=""" + Validations for all args + + Specifies validations for all arguments. Can be + used in conjunction with `arg`. If this node is + not present, and if there are no `arg` nodes, no + arguments will be allowed on the node at all + """ { + ref about-mixin + ref value-validation + children { + // TODO: opportunity for mutual requirements here + node min about=""" + Minimum argument count + + Minimum number of arguments that must be + present in a node. Must be less than or + equal to `max`, if the latter is + present. + """ { + arg { + gte 0 + type integer + } + } + node max about=""" + Maximum argument count + + Maximum number of arguments that may be + present in a node. Must be greater than or + equal to `max`, if the latter is present. + """ { + arg { + gte 0 + type integer + } + } + node distinct about=""" + All arguments must be distinct + + If present, all of this node's arguments + need to be distinct values. + """ { + arg { + type boolean + default #true + } + } + } + } + node children { + ref children-node + } + } + } + } + } + } + } +} +definitions { + node lang-mixin id=lang-mixin { + prop lang id=metadata-lang about=""" + Content language + + The (human) language of the text. + """ { + type string + } + } + node string-validations id=string-validations about="String-related validations" { + ref shared-validations + children { + node pattern about=""" + Regex-based validations + + Tests string values against a regular expression and passes if + the regular expression matches. + + Implementations SHOULD use an EcmaScript-compatible regex engine. If they choose not to, this SHOULD be clearly documented. + """ { + args { + min 1 + type string + } + } + node min-length about=""" + Minimum string length + + Minimum length of the value, if it's a string. + """ { + arg { + gte 0 + type integer + } + } + node max-length about=""" + Maximum string length + + Maximum length of the value, if it's a string. + """ { + arg { + gte 0 + type integer + } + } + node format about=""" + Specifies the format of the value + + Any supported type annotation from the KDL spec may be + specified. It is up to implementations whether they validate + this node. They SHOULD document the ones they support, if any. + + Any format that the implementation supports MUST be compliant + with the specified reserved format in the KDL spec, and only + apply it to the specified data types (e.g. `u8` can only apply + to items of type `integer`, not to `string` or `number`). If the + checked value is not of an applicable type, the implementation + MUST skip applying this to the given type. It MAY choose to warn + about skipping the format check. + + If a value specifies multiple `type`s, any `format`s are checked + as usual against the matrix of compatible `type`/`format` + values. + + Implementations MAY choose either error or simply warn about + format violations. They SHOULD document the behavior, and MAY + provide configuration for it. + """ + repeatable + args { + min 1 + type string + // https://json-schema.org/understanding-json-schema/reference/string.html#format + // TODO: Make sure this is up to date with the types listed in the spec. + enum \ + // String validations + date-time date time duration decimal currency country-2 \ + country- country-subdivision email idn-email hostname \ + idn-hostname ipv4 ipv6 url url-reference irl \ + irl-reference url-template regex uuid kpath \ + + // Number validations + i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 isize usize f32 \ + f64 decimal64 decimal128 \ + { + allow-others + } + } + } + node media-type about=""" + MIME type + + MIME type of string value. May be applied to 'deserialized' data + if value format is base64/base85 or some other stringly binary + encoding. + """ { + repeatable + args { + min 1 + type string + } + } + } + } + // Number-specific validations + node number-validations id=number-validations { + ref shared-validations + children { + node div about=" + Divisible by + + Constrains them to be multiples of the given number(s). Only + used for numeric values. If multiple numbers are given, _any_ + match will pass. In order to say something like `divisible by 3 + AND by 4`, use multiple `div` nodes: `div 3; div 4`. + """ { + repeatable + args { + min 1 + type number + } + } + node gt about=""" + Greater than + + Only used for numeric values. Constrains them to be greater than + the given number. + """ { + arg { + type number + } + } + node gte about=""" + Greater than or equal to + + Only used for numeric values. Constrains them to be greater than + or equal to the given number. + """ { + arg { + type number + } + } + node lt about=""" + Less than + + Only used for numeric values. Constrains them to be less than + the given number. + """ { + arg { + type number + } + } + node lte about=""" + Less than or equal to + + Only used for numeric values. Constrains them to be less than or + equal to the given number + """ { + arg { + type number + } + } + } + } + // Validations shared across all types. + node shared-validations id=shared-validations { + children { + node type about="The type for this value\n\nMultiple arguments signify a sum type." { + repeatable + args { + min 1 + type string + enum string boolean number integer #null + distinct + } + } + // TODO: establish equality expectations. + node const about=""" + Specific value + + Exact value that this value must match. Equivalent to a + single-value `enum` validation. + """ { + arg about="Constant value" + } + node enum about=""" + Enumeration of values + + An enumeration of possible values + """ { + repeatable + args about="Enumeration choices" { + min 1 + } + children { + node allow-others about=""" + Allow other choices + + Whether other values than those explicitly enumerated + may be provided, so long as they pass other validations + in the node. + + While apparently redundant, this option may be useful in + cases where there's a set of suggested values, but + others are acceptable. This information can then be used + by tooling to e.g. suggest completion items. + """ { + arg { + type boolean + default #true + } + } + node - about="Enumeration choice" { + prop description about="Documentation for this enumerated item." + arg description="Enum value" + } + } + } + } + } + // General value validations + node value-validations id=value-validations { + ref string-validations number-validations + children { + node annotations about=""" + Validates value type annotations + + String validations for the type annotations that can be applied + to this value. + """ { + ref string-validations + } + node default about=""" + Default value + + Sets a default value when optional. That is, it requires + `optional` for `arg` nodes, and doesn't do anything useful if a + `prop` is marked `required`, though it is not invalid to do so. + """ { + arg + } + } + } + node about-mixin id=about-mixin { + prop about about=""" + Description for this component. + + By convention, the format of this value is intended to be similar to + git's commit message system: The first line is treated as a short + descriptor/summary, and any lines underneath it are treated as the + longer-form documentation. Tooling SHOULD only display some or all + of the first line in user interfaces that call for terseness, and + they SHOULD display both the short descriptor and the longer + explanation + """ { + type string + } + children { + node about about=""" + Description for this component. + + By convention, the format of this value is intended to be similar to + git's commit message system: The first line is treated as a short + descriptor/summary, and any lines underneath it are treated as the + longer-form documentation. Tooling SHOULD only display some or all + of the first line in user interfaces that call for terseness, and + they SHOULD display both the short descriptor and the longer + explanation + + If both an `about` property and an `about` child node are + present in a definition, the child node's value MUST take + precedence. + """ { + arg { + type string + } + } + } + } +}