In this section we introduce two homomorphic transformation of RAML types that can be used to obtain a canonical representation of the type.
A RAML type expressed in expanded form has the following properties:
- All type expressions have been expanded
- All type names have been replaced by their expanded forms
- All nested types in the RAML type have an explicit type property that can be a built-in type name string or the declaration of a expanded RAML type (single inheritance) or array of RAML types (multiple inheritance)
- All facets with default values like
required
are made explicit - All nullable property values have been replaced by unions
The algorithm takes as input a RAML type form and a map of type bindings, a dictionary mapping type names (strings) into type expressions. The output of the algorithm is the expanded form for the RAML type.
Before describing the algorithm, we need to describe the data model for a RAML type form.
A RAML type form is defined using the following algebraic data types:
(constructor Record [a1:String f1:RAMLForm, ..., an:String fn:RAMLForm])
(constructor Seq [a1:RAMLForm, ... an:RAMLForm])
(constructor Scalar String | Integer | Boolean | $recur | ...)
(constructor RAMLForm Scalar | Record | Seq)
Since we are going to expand all type expressions, we need to provide a form representation for RAML union type, not currently defined in the spec:
(constructor Union [a1:RAMLForm, an:RAMLForm] (Record "type" "union", "of" (Seq a1 ... an)))
RAML types can be recursive, at the same time they are anonymous, no identifier for a type exist. To address both problems, we introduce another type form to designate a fixpoint recursion:
(constructor Fixpoint RAMLForm)
Where the recursion point is marked by the scalar type $recur
. Also note that there are not nesting restrictions on fixpoints.
It is trivial to create a mapping for a concrete syntax (JSON, XML, RAML) into these data types.
For example, provided the following definition of RAML types
#%RAML 1.0 Library
types:
Song:
properties:
title: string
length: number
Album:
properties:
title: string
songs: Song[]
The output of expanding the Album
type is the following
{"type" "object"
"properties" {"title" {"type" "string"
"required" true}
"songs" {"type" "array"
"items" {"type" "object"
"properties" {"title" {"type" "string" "required" true}
"length" {"type" "number" "required" true}}
"additional-properties" true
"required" true}
"required" true}}
"additional-properties" true
"required" true}
A recursive List
type like:
types:
List:
cell: Cell
Cell:
properties:
car: any
cdr: List | nil
Will be expanded in the following form:
{:type :fixpoint,
:value {:type "object",
:properties {"cell" {:properties {"car" {:type "any", :required true},
"cdr" {:anyOf [{:type :$recur, :required true}
{:type "nil", :required true}],
:type "union",
:required true}},
:additionalProperties true,
:type "object",
:required true}},
:additionalProperties true,
:required true}}
The pseudo-code for the transformation is the following:
The input for the algorithm is:
form
The form being expandedbindings
An object holding a mapping from user-defined RAML type names to RAML type formsoptions
An object holding a mapping of algorithm option names to their values:topLevel
A string with the default RAML type whose base type is not explicit and cannot be inferred. It can beany
orstring
, depending if the the type comes from thebody
of RAML service definition or any other node. The default value isany
.trackOriginalType
Indicates whether to track original user-defined RAML type names in theoriginalType
key whenever they are expanded. The default value isfalse
.callback
The callback function to be called when performing expansion asynchronously. The default is synchronous expansion.
Algorithm
- if
form
is aString
- if
form
is a RAML built-in data type, we return(Record "type" form)
- if
form
is a Type Expression, we return the output of calling the algorithm recursively with the parsed type expression and the providedbindings
- if
form
is a key inbindings
:- If the type hasn't been traversed yet:
- We return the output of invoking the algorithm recursively with the value for
form
found inbindings
and thebindings
mapping and we add the type to the current traverse path - If
trackOriginalType
istrue
inoptions
, the keyoriginalType
in the returned type should be set to the value forform
- We return the output of invoking the algorithm recursively with the value for
- If the type has been traversed:
- We mark the value for the current form as a fixpoint recursion:
$recur
- We find the container form matching the recursion type and we wrap it into a
(fixpoint RAMLForm)
form.
- We mark the value for the current form as a fixpoint recursion:
- If the type hasn't been traversed yet:
- else we return an error
- if
- if
form
is aRecord
- we initialize a variable
type
- if
type
has a defined value inform
we initializetype
with that value - if
form
has aproperties
key defined, we initializetype
with the valueobject
- if
form
has aitems
key defined, we initializetype
with the valueobject
- otherwise we initialise
type
with the value oftopLevel
inoptions
- if
- if
type
is aString
with valuearray
- we initialize the value
expanded-items
with the result of invoking the algorithm on the value inform
for the keyitems
(orany
if the keyitems
is not defined) - we replace the value of the key
items
inform
byexpanded-items
- we initialize the value
- if
type
is aString
with valueobject
- we iterate over the
Record
associated to the keyproperties
inform
- for each pair
(Seq property-name property-value)
in the record associated to the keyproperties
inform
- we initialize the variable
expanded-property-value
with the value of invoking the algorithm with input argumentsproperty-value
and the providedbindings
- we initialize the variable
- if the string
property-name
finishes with a?
string- we remove the key
property-name
from theproperties
record - we replace the
?
character inproperty-name
by the empty string and - we re-assigned the value of
expanded-property-value
by the union(Union expanded-property-value (Record "type" "null"))
- we remove the key
- if the output
Record
assigned toexpanded-property-value
does not have arequired
key, we assign a valuetrue
to the key - we re-assigned the key
property-name
in theproperties
Record
to the computed value forexpanded-property-value
- for each pair
- if the
Record
inform
does not have a defined value for the keyadditional-properties
, we assign a valuetrue
to the keyadditional-properties
- we iterate over the
- if
type
is aString
with valueunion
- we iterate through all the values stored in the key
of
inform
of typeSeq[RAMLForm]
- we initialize the variable
i
with the position of the value we are iterating andelem
with the current value - we initialize the variable
expanded-elem
with the output of invoking the algorithm with input argumentselem
and the provided map of bindings - we replace the value at position
i
in the sequenceof
by the the computedexpanded-elem
- we initialize the variable
- we iterate through all the values stored in the key
- if
type
is aString
with value inbindings
- we recursively expand forms nested within
properties
,of
, oritems
inform
- we return the output of invoking the algorithm on the value of
type
with the current value forbindings
- we recursively expand forms nested within
- if
type
is aRecord
- we recursively expand forms nested within
properties
,of
, oritems
inform
- we return the output of invoking the algorithm on the value of
type
with the current value forbindings
- we recursively expand forms nested within
- if
type
is aSeq[RAMLForm]
- we recursively expand forms nested within
properties
,of
, oritems
inform
- we iterate through all the values in
type
- for initialise the variable
i
with the position of the value we are iterating andelem
with the current value - we initialise the variable
expanded-type
with the output of invoking the algorithm onelem
withbindings
- we replace the element
i
intype
with the computedexpanded-type
- for initialise the variable
- we recursively expand forms nested within
- we return the new value for
form
- we initialize a variable
An example Clojure implementation of this algorithm can be found in the api-model.parsers.raml.expanded-form
namespace in this project.
A RAML type expressed in canonical form has all the properties of RAML type in expanded form plus the following:
- All
type
properties have aString
value - All inheritance relationships have been resolved
- All the constraints defined for the type are valid
- Unions can only appear at the top level of the type form ("union hoisting", which can be disabled)
The canonical form can be used to represent a RAML type in a unique way. It can also be used to perform validation since inheritance and union types have been resolved.
In order to work with inheritance over RAML types we will consider standard set inclusion semantics, where saying that type A
is a sub-type of B
means that all instances of A
are included in the domain of the type B
.
The algorithm we show in the following section computes the canonical form for a RAML type. It takes as input a RAML type in expanded form and produces the canonical form as output or throws an error if an inconsistency structurally or in a constraint is found.
For example, provided the following (not expanded) RAML type:
properties:
a: string
b: number | string
The computed canonical form of the type is:
{"type" "union",
"required" true,
"of" [{"type" "object"
"properties" {"a" {"type" "string", "required" true},
"b" {"type" "number", "required" true}},
"additional-properties" true,
"required" true}
{"type" "object",
"properties" {"a" {"type" "string", "required" true},
"b" {"type" "string", "required" true}},
"additional-properties" true,
"required" true}]}
The input of the algorithm is:
expanded-form
the form being processed of typeRecord[String][RAMLForm]
Algorithm
- we initialize the variable type with the value of the property
type
ofexpanded-form
- if
type
is in the setany boolean datetime datetime-only number integer string null file xml json"
- we return the output of applying the
consistency-check
to theform
- we return the output of applying the
- if
type
is the stringarray
- we replace the property
items
inform
with the output of applying the algorithm to the value of the keyitems
of the inputform
(orany
if the keyitems
is not defined) - we return the output of applying the
consistency-check
algorithm to the new value ofform
- we replace the property
- if
type
is the stringobject
- we initialize the variable properties with the value of the
properties
key inform
- we initialize the variable
accum
with the cloned value ofform
- we reset the key
properties
inaccum
to an empty record - for each pair
property-name
andproperty-value
in the variableproperties
- we initialize the variable
tmp
with the output of invoking the algorithm over the value inproperty-value
- if the property
type
oftmp
has the valueobject
- we add the pair
property-name
tmp
to theproperties
keys in each record inaccum
- we add the pair
- if the property
type
oftmp
has the valueunion
and union hoisting is not disabled- we initialize the variable
new-accum
to the empty sequence - for each value
elem-of
in the propertyof
oftmp
- for each value
elem
inaccum
- we clone
elem
- we clone
tmp
asnew-value
, except forof
, and assign the properties ofelem-of
to it - we add the pair
property-name
new-value
to the keyproperties
of the clonedelem
- we add the cloned
elem
to the sequencenew-accum
- we clone
- for each value
- we replace
accum
withnew-accum
- we initialize the variable
- if
accum
contains a single element- we return the output of applying the
consistency-check
algorithm to the only element inaccum
- we return the output of applying the
- if
accum
contains more than one element- we replace the
type
ofform
withunion
- we remove the keys
properties
andadditionalProperties
- we add the key
of
with the value ofaccum
- we return the output of applying the
consistency-check
algorithm to the modified value ofform
- we replace the
- we initialize the variable
- we initialize the variable properties with the value of the
- if
type
is the stringunion
- we recursively canonicalize forms nested within
of
inform
- we return the output of applying the
consistency-check
algorithm to the modified value ofform
- we recursively canonicalize forms nested within
- if
type
is aRecord[String][RAMLForm]
- we initialize the variable
super-type-name
to the first value of type string in the chain of nested records for the valuetype
starting with the one assigned totype
inform
- if
super-type-name
has a valuearray
we transformform
adding the propertyitems
pointing a record(Record "type" "any")
- if
super-type-name
has a valueobject
we transformform
adding the propertyproperties
with the empty record(Record)
- if
super-type-name
has a valueunion
we transformform
adding the propertyof
with the empty sequence(Seq)
- we initialize the variable
canonical-super-type
to the output of applying the algorithm to the value for the propertytype
inform
- we set the
type
property ofform
tosuper-type-name
- if
- we initialize the variable
tmp
with the output of invoking the algorithmmin-type
to the inputscanonical-super-type
andform
- we return the output of recursively applying the algorithm to the modified value of
tmp
- we initialize the variable
- if
type
isSeq[RAMLForm]
- we initialize the variable
super-type-name
to the first value of type string in the chain of nested records for the valuetype
starting with the one assigned totype
inform
- if
super-type-name
has a valuearray
we transformform
adding the propertyitems
pointing a record(Record "type" "any")
- if
super-type-name
has a valueobject
we transformform
adding the propertyproperties
with the empty record(Record)
- if
super-type-name
has a valueunion
we transformform
adding the propertyof
with the empty sequence(Seq)
- we initialize the variable
super-types
to the value for the propertytype
inform
- we set the
type
property ofform
tosuper-type-name
- if
- for each value
elem
insuper-types
- we initialize the variable
tmp
with the output of computing the result of invoking the algorithmmin-type
toelem
andform
- we re-assign
form
to the value computed intmp
- we initialize the variable
- we return the output of recursively applying the algorithm to the modified value of
tmp
- we initialize the variable
In the previous algorithm we have used two auxiliary algorithms min-type
and consistency-check
.
min-type
computes a canonical RAML type that will compute the biggest intersection between the sets defined super
and sub
. If such an RAML type is empty, an error will be thrown.
The input of the algorithm is:
super
the RAML canonical super-typesub
the RAML canonical sub-type
Algorithm min-type
- we initialize the variables
super-type
andsub-type
with the values of the propertiestype
ofsuper
andsub
respectively. - if
super-type
andsub-type
have the same value and the value is in the setany boolean datetime datetime-only number integer string null file xml json"
- we initialize the variable
computed
to the record with propertytype
having the commonsuper-type
andsub-type
value - for each restriction in
super
andsub
we compute the narrower restriction and we assign it incomputed
- for each restriction only in
super
orsub
we assign it directly tocomputed
- we return the output of computing the algorithm
consistency-check
oncomputed
- we initialize the variable
- if only one of
super-type
orsub-type
has a value ofany
- for each restriction in the
any
type and in the other type, we compute the narrower restriction and we re-assign it to the other type - for each restriction only in
any
we assign it directly to the other type - we return the output of computing the algorithm
consistency-check
on the other type
- for each restriction in the
- if
super-type
isnumber
and thesub-type
isinteger
- for each restriction in the
number
type and in theinteger
type, we compute the narrower restriction and we re-assign it to theinteger
type - for each restriction only in
number
we assign it directly to theinteger
type - we return the output of computing the algorithm
consistency-check
on theinteger
type
- for each restriction in the
- if
super-type
isarray
andsub-type
isarray
- we initialize the variable
min-items
with the output of applying this algorithm to the values for the keyitems
insuper
andsub
- we re-assign the value of the property
items
insub
with the value ofmin-items
- for each restriction in
super
andsub
we compute the narrower restriction and we assign it insub
- for each restriction only in
super
we assign it directly tosub
- we return the output of computing the algorithm
consistency-check
onsub
- we initialize the variable
- if
super-type
isobject
andsub-type
isobject
- for initialize the variable
common-props
to the empty record - for each key in the
properties
valuesub
that is also present in theproperties
value ofsuper
- we initialize the variable
tmp
with the output of applying the algorithm to the value for the common property insuper
and insub
- we assign the computed value using the name of the common property as the key in the
common-props
record
- we initialize the variable
- for each pair
property-name
property-value
only in eithersuper
orsub
we add it to the recordcommon-props
- for each restriction in
super
andsub
we compute the narrower restriction and we assign it insub
- for each restriction only in
super
we assign it directly tosub
- we assign the value of the key
properties
insub
to becommon-props
- we return the output of computing the algorithm
consistency-check
onsub
- for initialize the variable
- if
super-type
isunion
orsub-type
isunion
- initialize
computed
to the empty record - if
super-type
isunion
- assign its facets to
computed
- set
sup-of
toof
ofsup
- assign its facets to
- else
- assign the non-functional facets of
sup
tocomputed
and retain only the functional facets insup
- set
sup-of
to a single element array ofsup
- assign the non-functional facets of
- if
sub-type
isunion
- assign its facets to
computed
- set
sub-of
toof
ofsub
- assign its facets to
- else
- assign the non-functional facets of
sub
tocomputed
and retain only the functional facets insub
- set
sub-of
to a single element array ofsub
- assign the non-functional facets of
- initialize
of
ofcomputed
to the empty array - if
sup-of
is non-empty- if
sub-of
is non-empty- for each value
elem-super
insup-of
- for each value
elem-sub
insub-of
- set
result
to the output of applying this algorithm toelem-super
andelem-sub
- if
result
is aunion
, concatenate itsof
toof
ofcomputed
- else append
result
toof
ofcomputed
- set
- for each value
- for each value
- else if
sub-of
is empty, assignof
ofcomputed
tosup-of
- if
- else if
sup-of
is empty, assignof
ofcomputed
tosup-of
- for each restriction in unions
super
andsub
we compute the narrower restriction and we assign it incomputed
- for each restriction only in union
super
we assign it directly tocomputed
- for each restriction only in union
sub
we assign it directly tocomputed
- we return the output of computing the algorithm
consistency-check
oncomputed
- initialize
- else fail the algorithm due to incompatible types
In the previous algorithm we need to define how the narrower version of a constraint is computed. The following table provides the details:
property | valid | narrower |
---|---|---|
minProperties | (<= super sub) | (max super sub) |
maxProperties | (>= super sub) | (min super sub) |
minLength | (<= super sub) | (max super sub) |
maxLength | (>= super sub) | (min super sub) |
minimum | (<= super sub) | (max super sub) |
maximum | (>= super sub) | (min super sub) |
minItems | (<= super sub) | (max super sub) |
maxItems | (>= super sub) | (min super sub) |
format | (or (nil? super) (= super sub)) | (or super sub) |
pattern | (or (nil? super) (= super sub)) | (or super sub) |
discriminator | (or (nil? super) (= super sub)) | (or super sub) |
discriminatorValue | (or (nil? super) (= super sub)) | (or super sub) |
enumValues | (subset? sub super) | (intersection super sub) |
uniqueItems | (or (false? super) (= super sub)) | (and super sub) |
required | (or (false? super) (= super sub)) | (or super sub) |
additionalProperties | (or (false? super) (= super sub)) | (and super sub) |
If the valid condition in the previous table is not met, the min-type
algorithm will fail throwing an error.
Functional facets are those in the table above, as well as type
, properties
, items
, and anyOf
.
The other algorithm is consistency-check
. It just iterates through all the possible restriction constraints defined in the RAML specification and checks that the constraints hold for the provided type using custom logic. The check functions are:
check name | restrictions | check |
---|---|---|
num-properties | minProperties and maxProperties |
minProperties <= maxProperties |
length | minLength and maxLength |
minLength <= maxLength |
size | minimum and maximum |
minimum <= maximum |
num-items | minItems and maxItems |
minItems <= maxItems |
If any of the restrictions involved in the check are not defined, it automatically succeeds. In order to support additional restrictions will require to plug in the algorithm additional check functions or extend the structural type system here outlined to provide a generic way of expressing properties and there automatic check.
An example Clojure implementation of this algorithm can be found in the api-model.parsers.raml.canonical-form
namespace in this project.