Using Typed Model
This document contains instructions how to use Typed Model in different languages
Base Usage
TypeScript
Because TypeScript / JavaScript has very poor reflectional capabilities, a meta-model needs to be created at compile time, and this is done using TypeScript -decorators. First a complete example is shown and I then explain what all the parts mean:
import { Meta, Namespace, Value } from '@sisujs/common';
const NS = Namespace.of('library').init();
@NS.class('Author')
class Author {
@Value.string()
name: string
@(Value.of(Temporal.PlainDate))
birthdate: Temporal.PlainDate
}
@NS.class('Book')
class Book {
@Value.string()
title: string
@Value.string()
isbn: string
@(Value.of(Author))
author: Author
@Value.int32()
publicationYear: number
@Value.number()
rating: number
}
@NS.class('Catalog')
class Catalog {
@Value.string()
genre: string
@(Value.array().of(Book))
books: Book[]
@(Value.set().of(Author))
authors: Set<Author>
}
NS.seal([
Author,
Book,
Catalog
]);
const cbot = Cbot.getInstance({
namespaces: [NS],
temporalApiEnabled: true
});
Declaring Namespace(s)
In the example a namespace is created using pattern Namespace.of('name').init().
The init() function ensures that this is the first time namespace for the name
is declared thus preventing namespace pollution.
After all classes has been declared, it is important to seal() the namespace
and validate that all classes are declared. This is actually really important
due the nature how JavaScript is loaded. It can be seen just a big linear file
that loads stuff one after another. And it is possible to create a situation
where you are not actually sure what has been loaded into the namespaces.
Thus sealing the namespace ensures that only the classes you wanted are included
in the namespace.
Mapping Classes
Next classes are decorated using @NS.class('name') which tells what name
to use for the class within the namespace. Givin the name cannot be omitted
because during compilation there are no guarantiees what the actual type name
will be.
Mapping Inheritance
It is also possible to use abstract classes in following way:
@NS.abstractClass('Parent')
export abstract class Parent {
...
}
@NS.class('Child', Parent)
export class Child extends Parent {
...
}
Mapping Properties
Each property within the class must then be decorated to create a meta-model for them.
This is done using Value-decorator which offers all combinations that are currently supported.
Using Any-type
If you encounter a situation that the type you want to use or cannot be expressed via
the Value-decorator it can be migitated using Value.any()-type. For instance
current implementation does not have good support for declaring Maps so they can
simply be declared as any. This of course mean the you lose the support for
type checking at protocol level but it does allow data to be serialized
@NS.class('MyType')
export class MyType {
@Value.any()
map: Map<String, Author>
}
Java
In Java namespace is defined as:
import fi.sisujs.cbot.model.Namespace;
var ns = Namespace.of("my-namespace", "com.foo.model");
Here com.foo.model means a java-package and all classes within the package
or subpackages are included in the namespace. No other configurations are
needed since all properties and inheritance can be inferred through reflection.
A proper Cbot-instance is then instantiated as:
var cbot = Cbot.getInstance(
OptionsBuilder
.create()
.withNamespaces(List.of(ns))
.build());
Renaming and omitting properties
There are two important property annotations: CbotProperty and CboIgnore.
The first can be used to rename the model property to a different name and latter
to simply omit property from being serialized.
For instance:
public class MyClass {
@CbotProperty(name="renamed")
private String name;
@CbotIgnore
private Integer ignoreMe;
}
Upgrading Model
Model Upgrade is a concept where an existing model can be upgraded in such a way that existing clients can continue using the existing previous model while other clients may start using an upgraded version.
A model can be upgraded multiple times, although at some point it is necessary to reset the situation.
A Model can be upgraded in cases when:
- new Model Propertys are added to an Object Model
- new Model Objects are added to the model in such a way that they are used only in the new properties.
TypeScript
In typescript existing class is given an upgraded property in following way:
@NS.class('ToBeUpgraded')
export class ToBeUpgraded {
@Value.string()
unupgraded: string
@Value.int64()
@Value.cbotUpgrade(1)
upgraded: number
}
In this case upgraded-property is omitted when using the base model.
Model Objects themselves can also be marked to exist only in the upgraded model as:
@NS.class('NewClass')
@NS.cbotUpgrade(1)
class NewClass {
...
}
In both cases, client that are using base model do not touch these Model Objects or Model Propertys during deserialization.
A proper Cbot-instance is then instantiated as:
const cbot = Cbot.getInstance({
namespaces: [NS],
modelUpgrade: 1
});
Java
Java has a very similar way of defining upgraded models as in TypeScript, which can be summarized as:
public class ToBeUpgraded {
private String unupgraded;
@CbotProperty(upgrade = 1)
private Long upgraded;
}
@CbotType(upgrade = 1)
public class NewClass {
...
}
var cbot = Cbot.getInstance(
OptionsBuilder
.create()
.withModelUpgrade(1)
.withNamespaces(List.of(NS_V1)));
Partial Model Support
TODO
Interoperability via Model Configuration
When dealing with multiple clients, upgrades and implementations interoperability between them must be validated. In many other protocols this is done using a common type-mapping (ie. OpenAPI etc) to ensure compatibility. This protocol (so far) does not take such approach. This is because different programming languages have so many different ways of handling the same concepts that it may be too cumbersome to force a certain mapping for everything.
Instead, compatibility is checked using a Model Configuration. Each implementation has to functions to get a configuration in JSON-format and validate it. The configuration cannot be considered a schema but an internal representation of the schema.
Then during development a "master"-model can then be shared among all parties to match their implementation to the model.
TypeScript
In TypeScript model can be validated as follows:
const cbot = Cbot.getInstance({
namespaces: [NS],
temporalApiEnabled: true
});
// Get the configuration
const configuration = cbot.getModelConfiguration();
// Validate configuration
const isValid = cbot.validateConfiguration(configuration, false);
Java
In Java model can be validated as follows:
```java
var cbot = Cbot.getInstance(
OptionsBuilder
.create()
.withNamespaces(List.of(ns))
.build());
// Get configuration as model object
var configuration = cbot.getModelConfiguration();
// Or a JSON-string
String json = cbot.getModelConfigurationAsJSON();
// Validate configuration
var isValid = cbot.validateConfiguration(configuration, false);
// Or from JSON-string
var isValid = cbot.validateConfiguration(Cbot.toConfiguration(json), false);