Type Design
Aggregation
Where there is the unification of different type fields in the schema, we instead embed those fields as a separate type.
For example:
type MethodParameterDocumentation =
{ Name: string
Description: string
Required: bool
// Embedded
TypeInformation: TypeInformation }
The type above would be an interface that inherits/extends the properties of the
TypeInformation shape. In this case, it is easier to embed this as a separate
type, rather than spread its shape within the record.
Optional Fields vs Nullable Values
In cases where there is the distinction between a property that is always present
but may contain a null/undefined value, and a property that may not be present,
we annotate the field with // Optional Field to distinguish the latter.
type MethodDocumentationBlock =
{
// Embedded
DocumentationBlock: DocumentationBlock
// Optional field
RawGenerics: string option
Signature: string
Parameters: MethodParameterDocumentation[]
Returns: TypeInformation option }
This is reflected in the Decoder.
module MethodDocumentationBlock =
let decode: Decoder<MethodDocumentationBlock> =
Decode.object (fun get ->
{ DocumentationBlock = get.Required.Raw DocumentationBlock.decode
RawGenerics = get.Optional.Field "rawGenerics" Decode.string
Signature = get.Required.Field "signature" Decode.string
Parameters =
Decode.array MethodParameterDocumentation.decode
|> get.Required.Field "parameters"
Returns = Decode.option TypeInformation.decode |> get.Required.Field "returns" })
Notice that the
Returnsfield uses the.Requiredfield access; however, theRawGenericsfield uses the.Optionalfield access for thoth since it may not be present.Still, both have the same
Optiontype.
Unions
Where we have a possibility of one of several types distinguished by a field value, we assert the condition for the field value in the decoder, and then attempt each of the decoders in sequence, leaving the least specific to last.
module ElementDocumentationContainer =
let decode: Decoder<ElementDocumentationContainer> =
Decode.object (fun get ->
get.Required.Field "type" Decode.string
|> function
| "Element" ->
{ Process = get.Required.Field "process" ProcessBlock.decode
Methods = Decode.array MethodDocumentationBlock.decode |> get.Required.Field "methods"
Events = Decode.array EventDocumentationBlock.decode |> get.Required.Field "events"
Properties =
Decode.array PropertyDocumentationBlock.decode
|> get.Required.Field "properties"
BaseDocumentationContainer = get.Required.Raw BaseDocumentationContainer.decode }
| _ -> Decode.string |> Decode.andThen Decode.fail |> get.Required.Field "type")
module ParsedDocumentation =
let decode: Decoder<ParsedDocumentation> =
Decode.oneOf
[ ModuleDocumentationContainer.decode
|> Decode.map (Module >> ExtensionAssistant.add)
ClassDocumentationContainer.decode
|> Decode.map (Class >> ExtensionAssistant.add)
StructureDocumentationContainer.decode
|> Decode.map (Structure >> ExtensionAssistant.add)
ElementDocumentationContainer.decode
|> Decode.map (Element >> ExtensionAssistant.add) ]
// This example has no *catch all* at the end, because
// we want an exception to be raised if our schema doesn't
// fit.