diff --git a/.vscode/settings.json b/.vscode/settings.json index 3e40d53c..af355459 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,17 +1,24 @@ { "cSpell.words": [ "abnf", + "capability", "CSDL", "dotnet", + "endswith", "eset", + "Insertable", "jetsons", "nuget", "nupkg", "odata", "rapid", "rsdl", + "sku", + "Spacely", + "startswith", "svel", "tools", + "writeonly", "xunit" ], "dotnet-test-explorer.testProjectPath": "**/*tests.@(csproj|vbproj|fsproj)" diff --git a/docs/rsdl/rapid-rsdl-capabilities.md b/docs/rsdl/rapid-rsdl-capabilities.md new file mode 100644 index 00000000..9ece70a6 --- /dev/null +++ b/docs/rsdl/rapid-rsdl-capabilities.md @@ -0,0 +1,337 @@ +--- +id: rsdl-capabilities +title: Capabilities +sidebar_label: RSDL API Capabilities +--- + +# Path Centric Service Capabilities + +The previous sections described how to define the format of the request and response bodies based on types and their relationships. These structures also imply which URL paths are valid in the service: starting with the service properties and following properties of the structured types. For example in the service below, `orders` is a service property which allows access via the `/orders` URL. Since an order has multiple order items via the `items` property, the URL `/orders//items` and `/orders//items/` are also a valid URLs. + +Without further constraints this would allow a huge number of URLs that a service would need to support. And it is important not just to specify which paths are allowed, but also to specify different functionality and behaviors supported for different paths. + +The following sections introduce the notion of paths and capabilities that the service provides per path. + +## Example service + +For the examples in the section of this document, we assume the following type definitions. This is a simplified service that provides a top level orders collection together with their (contained) order items. Each item references a SKU, that can be accessed via the top level entity set. + +```rsdl +type Order { + key id: String + created: DateTime + status: OrderStatus + items: [OrderItem] +} + +enum OrderStatus { + Open + Archived + Canceled +} + +type OrderItem { + key id: String + sku: SKU + amount: Integer +} + +type SKU { + key id: String + name: String + description: String + unitPrice: Decimal +} + +service { + orders: [Order] + skus: [SKU] +} +``` + +The amount of potential URLs that the service may support is large - in larger services potentially unbounded. Just to name a few of the more important ones: + +``` http +/skus, /skus/{id}, /orders, /orders/{id}, /orders/{id}/items, /orders/{id}/items/{id}, +/orders/{id}/items/{id}/skus, /orders/{id}/items/{id}/skus/{id}, ... +``` + +The path-centric view in RSDL allows to enumerate the allowed requests and specify which query options are supported for these requests. + +## HTTP capabilities +The first level of the above mentioned capabilities is to specify the path of the URL and the HTTP method, Here is a partial RSDL to demonstrate this. + +``` rsdl +path /orders { + GET { ... } + POST { ... } +} + +path /orders/{id} { + GET { ... } + PATCH { ... } + DELETE { ... } +} + +path /orders/{id}/items { + GET { ... } + POST { ... } +} + +path /orders/{id}/items/{id} { + GET { ... } + DELETE { ... } +} + +path /skus { + GET { ... } +} + +``` +The effect of this is that for each declared `path` and HTTP method the service is expected to return data, in the form specified by the corresponding type. The service is free to respond with success messages for other combinations but the service guarantees to work for the ones listed. + +Important to notice is that the paths after the `path` keyword are not literal paths but sort of templates. Many of the interesting paths in a REST service have path segments that are entity ids. This is indicated via the `{id}` syntax: the `id` in there is exactly the name of the key property. + +The placeholders `...` are used to declare which query options are supported by the service. For example a GET capability lists that certain $filter options are allowed or that paging is supported via the $top, $skip options. More details in the next sections. + +The specific capabilities that can be used in the HTTP capabilities section instead of the `...` in the example above can vary by HTTP method. Here is an overview + +| | filter | expand | paging | count | +|--------|:--------------------:|:--------------------:|:--------------------:|:--------------------:| +| GET | ✓ | ✓ | ✓ | ✓ | +| POST | ✓1 | ✓1 | ✓1 | ✓1 | +| PATCH | ✓1 | ✓1 | ✓1 | ✓1 | +| PUT | ✓1 | ✓1 | ✓1 | ✓1 | +| DELETE | | | | | + +[1] to shape the POST/PATCH/PUT response. Rarely used but supported
+[2] deleting multiple items. Rarely used but supported
+[!!TODO: check if filter segment on delete is allowed in RAPID ] + +## Individual Query capabilities + +### Filter capabilities + +The filter capability allows to specify which property can be used in which filter expression. There is a vast amount of possible filter expressions (see [OData System Query Option `$filter`](https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionfilter)). Therefore, the filter capabilities allow to specify a few well-known but restrictive expressions or allow any expression. + +The format for filter capabilities is a sequence of pairs of a so called operator group and a list of property names. An operator group is constraining the form of the expression allowed in the $filter system query option. + +| operator group | comment | +|----------------|-----------------------------------------------------------------------------------------------------------------------| +| eq | ` eq ` or
` in (, , ... ) ` | +| range | ` le and le `
or equivalent expressions with operators `ge`, `gt`, `lt` | +| ranges | a disjunction of `range` expressions | +| prefix | `startswith(, )` | +| text | `(, )`,
where `` is one of `startswith`, `endswith`, `contains` | +| any | any expression including expressions combined with `not`, `and`, and `or` | + +In RSDL this + +```rsdl +path /orders { + GET { + filter { + eq { id name description createdDate fulfillmentDate } + ranges { createdDate description } + prefix { name } + text { description } + } + + filter { + eq except { description } + ranges { createdDate description } + prefix { name } + text { description } + } + } +} +``` + +### Expand capabilities + +The expand capability allows to specify which properties can be used in `$expand` query parameter. + +The format is a sequence of properties that can be expanded. + +```rsdl +path /orders { + GET { + expand { + items + } + } +} +``` + + The expand capability introduces a nesting of capabilities since the type of the expandable property can itself be used in select, filter, and expand. + +```rsdl + + +type Order { + +} + +capability Order defaultOrder { + filter { + eq { id name } + prefix { nam description } + range { createdDateTime } + } + expand { + items { + filter { + eq { id name } + prefix { name } + } + expand { + sku + } + } + } + paging + count +} + +capability Order limitedOrder { + filter { + eq { id name } + } +} + +path /orders { + GET { + defaultOrder + } +} + +path /customers/{id}/orders { + GET { + limitedOrder + } +} +``` + +- `/order?$expand=items` +- `/order?$expand=items(expand=sku)` +- `/order?$expand=items(expand=sku; filter=id eq '100')` +- `/order?$expand=items(select=id; expand=sku; filter=id eq '100')` + + +### Paging capabilities + +The paging capability allows to specify if a request returning a collection can specify $top and $skip query parameters. + +```rsdl +path /orders { + GET { + paging + } +} +``` + +The paging capability has no parameters itself. If a service uses a different way to implement paging, please refer to the [extensibility](#extensibility) section below + +The paging capability is typically seen in a GET capability but can also be nested in an expand capability. + +### Count capabilities + +The count capability allows to specify if a request returning a collection can specify $count query parameter. + +```rsdl +path /orders { + GET { + count + } +} +``` + +## Extensibility + +The RSDL API capabilities allow to specify custom capabilities, so called traits, for the service to implement that are outside the scope of the patterns known by the RSDL specification. + +One example for these could be the handling of requests when throttling is needed, where the service responds with certain headers for requests that cross a throttling threshold. There are multiple established pattern (protocol) to implement this but it is not covered in RSDL directly. But it can be specified in RSDL as an extension and the service implementation can read it and configure the right behavior based on the traits in RSDL. + +The individual trait is treated as simple opaque (meta-) data that is passed on the service in the form of a trait name and a list of key-value pairs. The implementation is then free to interpret the presence of the trait and the parameters as it sees fit. + +```rsdl +path /orders { + GET { + traits { + longRunningOperation + topWithoutSkip + throttling { level: "premium\u12a4" } + } + } +} +``` + + +## Other path based characteristics + +```rsdl +path /user/{id}/recentOrders { + # references an Order contained in /orders + targets: /orders + targets: unbound + # 'targets' ':' ( path / 'unbound' ) + + GET { } +} + +path /orders { + # top level entity set + GET { } + +} + +path /orders/{id}/items { + # containment navigation (informally an entity set) + GET { } + +} +``` + + + + +## Syntax + +!! NOTE: incomplete + +``` abnf +path-capability = "path" path "{" + [ get-capabilities ] + [ post-capabilities ] + [ patch-capabilities ] + [ delete-capabilities ] + "}" + +query-capabilities = [ expand-capabilities ] / + [ filter-capabilities ] / [ select-capabilities ] / + [ paging-capabilities ] / [ count-capabilities ] / + +get-capabilities = "GET" "{" [ query-capabilities ] "}" + +post-capabilities = "POST" "{" "}" + +patch-capabilities = "PATCH" "{" "}" + +delete-capabilities = "DELETE" "{" "}" + +expand-capabilities = "expand" ["{" + *( property-name "{" query-capabilities "}" ) + "}"] + +filter-capabilities = "filter" ["{" operator-filter "}"] +operator-filter = operator-group "{" *property-name "}" +operator-group = "eq" / "range" / "ranges" / "prefix" / "strings" / "any" + +paging-capabilities = "paging" ["{" "}"] +count-capabilities = "count" ["{" "}"] + +property-name = identifier +path = "`" *( "/" identifier ) "`" + +``` + diff --git a/docs/rsdl/rapid-rsdl-capability-mapping.mdx b/docs/rsdl/rapid-rsdl-capability-mapping.mdx new file mode 100644 index 00000000..b43ca13e --- /dev/null +++ b/docs/rsdl/rapid-rsdl-capability-mapping.mdx @@ -0,0 +1,220 @@ +--- +id: rsdl-capability-mapping +title: Capability semantics +sidebar_label: RSDL API Capability semantics +--- + +> DRAFT +> Initial Draft. Dec 2021 + +Mapping RSDL capabilities to CSDL annotations + +## Delete + +```rsdl +path /skus/{id} { + DELETE +} +``` + +```xml + + + + + + + +``` + +## Update + +```rsdl +path /skus/{id} { + PATCH { + excluded { id releaseDate } + } +} +``` + +```xml + + + + + + + id + releaseDate + + + + + +``` + +## Insert + +```rsdl +path /orders { + POST { + required { externalId } + excluded { id orderDate } + } +} +``` + +```json +{ + "$Annotations": { + "orders": { + "@Org.OData.Capabilities.V1.InsertRestrictions": { + "$Type": "Org.OData.Capabilities.V1.InsertRestrictionsType", + "RequiredProperties": ["id", "orderDate"], + "NonInsertableProperties": ["id", "orderDate"] + } + } + } +} +``` + +```xml + + + + + + + id + orderDate + + + + + id + orderDate + + + + + +``` + +## Paging + +```rsdl +path /skus { + GET { + paging + } +} +``` + +```xml + + + + +``` + +## Filtering + +```rsdl +path /orders { + GET { + filter { + eq { id name description createdDate fulfillmentDate } + ranges { createdDate description } + prefix { name } + text { description } + } + } +} +``` + +```javascript +"$Annotations": { + "orders": { + "@Org.OData.Capabilities.V1.FilterRestrictions": { + "$Type": "Org.OData.Capabilities.V1.FilterRestrictionsType", + "Filterable": true, + "FilterExpressionRestrictions": [ + { + "$Type": "Org.OData.Capabilities.V1.FilterExpressionRestrictionType", + "Property": "id", + "AllowedExpressions": "SingleValue" + }, + { + "$Type": "Org.OData.Capabilities.V1.FilterExpressionRestrictionType", + "Property": "name", + "AllowedExpressions": "SingleValue" + }, + // some FilterExpressionRestrictionType removed for readability + { + "$Type": "Org.OData.Capabilities.V1.FilterExpressionRestrictionType", + "Property": "description", + "AllowedExpressions": "SearchExpression" + } + ] + } + } +``` + +### Filter restrictions + +The RSDL filter restriction is a simplified version of the EDM filter restrictions. +Instead of listing each pair of property/filter-restrictions, properties are group together by filter restriction. +e.g. `eq { a b } prefix { c }` in rsdl is represented in CSDL as a "flattend" list of pairs `eq: a, eq: b, prefix: c` + +The keywords for the filter restrictions are also simplified: + +| RSDL | CSDL | | +| :------: | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `eq` | `SingleValue` | Compare a property and a constant value with the eq operator | +| `in` | `MultiValue` | Compare a property with the in operator and a set of constant values | +| `range` | `SingleRange` | Compare a property with the open interval (i.e. `a < p and p < b`) where a and b are constants and p is the property. the `<` can be any of the comparison operators `<`, `<=`, `>`, `>=` | +| `ranges` | `MultiRange` | One of more `range` expressions combined by or | +| `prefix` | a specialization of `SearchExpression` | Compare a property with a constant using the startsWith function | +| `text` | `SearchExpression` | Compare a property with a constant using any of the string comparison functions startsWith, contains, endsWith | +| -/- | `MultiRangeOrSearchExpression` | | +| `any` | no restrictions | Any combination of the above using `and`, `or`, and `not` and any of the built-in functions | + +## Expansion + +```rsdl +path /orders { + GET { + expand { + items { + expand { + sku + } + } + } + } +} +``` + +```xml + + + + + + + + + + + + + + + +``` + +## References + +- [OData Capability Vocabulary](https://github.com/oasis-tcs/odata-vocabularies/blob/main/vocabularies/Org.OData.Capabilities.V1.md) + +- [Filter Restrictions](https://github.com/oasis-tcs/odata-vocabularies/blob/main/vocabularies/Org.OData.Capabilities.V1.xml#L410:~:text=%3CTerm%20Name=%22-,FilterRestrictions,-%22) diff --git a/docs/rsdl/rapid-rsdl-semantics.mdx b/docs/rsdl/rapid-rsdl-semantics.mdx index c13cd00d..5938f426 100644 --- a/docs/rsdl/rapid-rsdl-semantics.mdx +++ b/docs/rsdl/rapid-rsdl-semantics.mdx @@ -4,19 +4,19 @@ title: RSDL Semantics --- > DRAFT -> Initial Draft. July 2020 +> Initial Draft. June 2022 The semantic of RSDL (RAPID Schema Definition Language) can be described by mapping -syntactical constructs described in [rapid-rsdl-abnf](rsdl-abnf) to equivalent runtime [Service Definition](../spec/rapid-pro-resource_description.md) constructs. +syntactical constructs described in [RSDL ABNF](../rsdl-abnf) to equivalent runtime [Service Definition](../spec/rapid-pro-resource_description.md) constructs. -Please refer to [rapid-rsdl-abnf](./rsdl-abnf) for the syntactical constructs of RSDL. +Please refer to [RSDL ABNF](../rsdl-abnf) for the syntactical constructs of RSDL. import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; ## Model -A [model](rsdl-abnf#model) is mapped to a CSDL Schema named "Model", that has an entity container named "Service". +A [model](../rsdl-abnf#model) is mapped to a CSDL Schema named "Model", that has an entity container named "Service". The model's -[service](rsdl-abnf#service), -[structured types](rsdl-abnf#structured-type), and -[enumeration types](rsdl-abnf#enumeration-type) are mapped to the respective constructs below and added to the schema (or container respectively) +[service](../rsdl-abnf#service), +[structured types](../rsdl-abnf#structured-type), and +[enumeration types](../rsdl-abnf#enumeration-type) are mapped to the respective constructs below and added to the schema (or container respectively) ## Structured Types -A [structured type](rsdl-abnf#structured-type) is mapped to either +A [structured type](../rsdl-abnf#structured-type) is mapped to either an [entity type](http://docs.oasis-open.org/odata/odata-csdl-xml/v4.01/odata-csdl-xml-v4.01.html#sec_EntityType) or a [complex type](http://docs.oasis-open.org/odata/odata-csdl-xml/v4.01/odata-csdl-xml-v4.01.html#sec_ComplexType) when converting from RSDL to CSDL. @@ -296,7 +296,7 @@ type Company { The type of a property is one of: -- the built-in types defined in the [`builtInType`](rsdl-abnf/#structured-type) syntax rule +- the built-in types defined in the [`builtInType`](../rsdl-abnf/#structured-type) syntax rule - the primitive EDM types listed in [OData CSDL JSON Representation](http://docs.oasis-open.org/odata/odata-csdl-json/v4.01/odata-csdl-json-v4.01.html#sec_PrimitiveTypes) and [OData CSDL XML Representation](http://docs.oasis-open.org/odata/odata-csdl-xml/v4.01/odata-csdl-xml-v4.01.html#sec_PrimitiveTypes) - the structured or enumeration types defined in the model @@ -525,7 +525,7 @@ type Employee { ## Enumeration Types -An [Enumeration Type](rsdl-abnf#enumeration-type) is mapped to a CSDL EnumType. The enumeration members' values are automatically assigned. +An [Enumeration Type](../rsdl-abnf#enumeration-type) is mapped to a CSDL EnumType. The enumeration members' values are automatically assigned. ```json enum employmentType { salaried hourly } @@ -564,7 +564,7 @@ enum employmentType { salaried hourly } ## Flag Types -A [Flags Type](rsdl-abnf#enumeration-type), i.e an enumeration that starts with the keyword flags, is mapped to an CSDL EnumType with the `IsFlags` property set to `true`. +A [Flags Type](../rsdl-abnf#enumeration-type), i.e an enumeration that starts with the keyword flags, is mapped to an CSDL EnumType with the `IsFlags` property set to `true`. The members' values are automatically assigned to powers of 2. ```json diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index 24d7a338..0aa345ba 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -6,6 +6,7 @@ module.exports = { favicon: 'img/rest.svg', organizationName: 'oasis-open', // Usually your GitHub org/user name. projectName: 'odata-rapid', // Usually your repo name. + trailingSlash :true, themeConfig: { prism: { theme: require('prism-react-renderer/themes/oceanicNext'), diff --git a/website/sidebars.js b/website/sidebars.js index 93b409e7..0a553f7a 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -26,6 +26,8 @@ module.exports = { 'rsdl/rsdl-intro', 'rsdl/rsdl-semantics', 'rsdl/rsdl-abnf', + 'rsdl/rsdl-capabilities', + 'rsdl/rsdl-capability-mapping' ] }, { diff --git a/website/src/theme/prism-include-languages.js b/website/src/theme/prism-include-languages.js index 65d449c4..6b75a9e5 100644 --- a/website/src/theme/prism-include-languages.js +++ b/website/src/theme/prism-include-languages.js @@ -17,8 +17,9 @@ const prismIncludeLanguages = (PrismObject) => { additionalLanguages.forEach((lang) => { require(`prismjs/components/prism-${lang}`); // eslint-disable-line }); - - require('./rsdl/prism-rsdl.js'); + + require(`prismjs/components/prism-abnf`) + require(`./prism-rsdl.js`); delete window.Prism; } diff --git a/website/src/theme/rsdl/prism-rsdl.js b/website/src/theme/prism-rsdl.js similarity index 70% rename from website/src/theme/rsdl/prism-rsdl.js rename to website/src/theme/prism-rsdl.js index 68c96c5c..b636d5cc 100644 --- a/website/src/theme/rsdl/prism-rsdl.js +++ b/website/src/theme/prism-rsdl.js @@ -2,23 +2,21 @@ Prism.languages.rsdl = { 'keyword': [ { - pattern: /\b(?:type|enum|service|abstract|open|key|extends|path)\b/, + pattern: /\b(?:type|enum|service|abstract|open|key|extends|path|capability)\b/ }, { - pattern: /\b(?:GET|POST|PATCH|DELETE|expand|filter|paging|count)\b/, + pattern: /\b(?:GET|POST|PATCH|DELETE|expand|filter|paging|count|required|excluded|traits)\b/ }, { - pattern: /\b(?:eq|range|prefix|text|any)\b/, + pattern: /\b(?:eq|range|ranges|prefix|text|any)\b/ } ], - 'property': [ - /[-\w\.]+(?=\s*=(?!=))/, - /"(?:\\[\s\S]|[^\\"])+"(?=\s*[:=])/, - ], + 'string': [ { - pattern: /"""[ \t]*[\r\n](?:(?:"|"")?(?:\\.|[^"\\]))*"""/, - greedy: true, + // javascript strings https://www.json.org/json-en.html + pattern: /(?:"(?:[^"\\]|\\["\\\/bfnrt]|\\u[0-9A-Fa-f]{4})*")/, + greedy: true } ], 'description': { @@ -42,6 +40,6 @@ Prism.languages.rsdl = { }, 'number': /\b0x[\da-f]+\b|\b\d+(?:\.\d*)?(?:e[+-]?\d+)?/i, 'boolean': /\b(?:true|false)\b/i, - 'punctuation': /[\[\]{}:]/, + 'punctuation': /[\[\]{}():]/, 'symbol': /[=]/, }; \ No newline at end of file