-
Notifications
You must be signed in to change notification settings - Fork 54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rd-gen docs #481
base: master
Are you sure you want to change the base?
Rd-gen docs #481
Changes from all commits
a1b38ad
dbca021
659734f
df30f69
74ade3d
3965d34
ff73d6a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,249 @@ | ||
How to Use RdGen | ||
================= | ||
RdGen is a tool to generate model serialization and deserialization code in multiple languages (all compatible with each other): C#, Kotlin, C++. | ||
|
||
To define a model for RdGen, a domain-specific language (DSL)-styled library for Kotlin is provided. | ||
|
||
To invoke RdGen, Gradle build system is used in most cases. | ||
|
||
Basic Terminology | ||
----------------- | ||
Each connection use a separate set of models, collected under a _root model_. The root model can be extended via _extension models_, while each of the extensions may contain their own extensions as well, forming a protocol tree. | ||
|
||
In the runtime, the set of the models is asynchronously replicated between both the sides of a connection (and Rd connections are always two-sided). | ||
|
||
The _root model_ and all the _extension models_ are collectively called _top-level models_, because they are defined on the top of the file in RdGen, and have a common set of properties, such as the ability to contain other entities. | ||
|
||
Each of these models may have a set of _members_, such as: | ||
- read-only members | ||
- constants | ||
- fields | ||
- reactive members (i.e. the members that have a notification mechanism either side of the connection may _react_ to) | ||
- properties | ||
- lists | ||
- maps | ||
- sets | ||
- callable members | ||
- signals (sinks / signal sources): "fire and forget" | ||
- calls, callbacks: it's possible to return a result | ||
|
||
The protocol allows use for persistent models that have mutable members in memory, as well as in a RPC-like way without any persistence via signals and calls. | ||
|
||
Model Definition | ||
---------------- | ||
In this section, we'll omit the following import statement on top of all the code examples: | ||
```kotlin | ||
import com.jetbrains.rd.generator.nova.* | ||
``` | ||
|
||
### Root and Extensions | ||
Each protocol definition starts from the root model: | ||
|
||
```kotlin | ||
object ExampleRoot : Root() | ||
``` | ||
|
||
This root object is a top-level model, and thus can contain members (see below), but it's also possible to add extensions on top of it: | ||
|
||
```kotlin | ||
object ExampleExt : Ext(ExampleRootNova) | ||
``` | ||
|
||
This extension is a separate model (you may imagine them like namespaces with an additional ability to hold members with static lifetime). | ||
|
||
You may put everything from your protocol into a root model, or define separate extensions for product subsystems and such. | ||
|
||
### Types | ||
Before we discuss the model members, let's discuss the member types. RdGen allows the user to define their own types that then may be used in models (and may be nested into each other). You define a type via several ways: | ||
|
||
1. By calling a corresponding method on a top-level object, for example: | ||
```kotlin | ||
object Example : Root() { | ||
val nestedType = structdef { | ||
// Nested members of the type go here | ||
} | ||
} | ||
``` | ||
|
||
This defines a struct named `nestedType` inside of an `Example` model. | ||
2. By calling a method in a place where a corresponding type is expected, in an ad-hoc manner, for example: | ||
```kotlin | ||
object Example : Root() { | ||
init { | ||
field("foobar", structdef("OptionalNameForThisType") { | ||
// Nested members of the type go here | ||
}) | ||
} | ||
} | ||
``` | ||
|
||
You can reuse a same type declared once in several places (for example, as a type for several fields or a return type of a call). | ||
|
||
#### Type Categories | ||
Most of the types from the coming section are split into two big categories: _bindable types_ and _scalars_. | ||
|
||
A bindable type is a type that can contain reactive members, while a scalar type is immutable and cannot contain any reactive or bindable members in it. | ||
|
||
#### Entity Types | ||
The protocol allows to declare the following type of entities: | ||
1. `structdef`: a simple _non-bindable type_ that cannot contain any reactive properties; only fields are allowed. Use structs for simple data, normally for value objects only. | ||
|
||
Structs support inheritance, use `basestruct` and `extends` construct if you need: | ||
```kotlin | ||
val BaseStruct = basestruct { | ||
field("baseField", PredefinedType.int) | ||
} | ||
val DerivedStruct = structdef extends BaseStruct { | ||
field("derivedField", PredefinedType.bool) | ||
} | ||
``` | ||
2. `classdef`: a bindable type that may contain any reactive members inside. Classes also support inheritance: | ||
```kotlin | ||
val BaseClass = baseclass { | ||
field("baseField", PredefinedType.int) | ||
} | ||
val DerivedClass = classdef extends BaseClass { | ||
field("derivedField", PredefinedType.bool) | ||
} | ||
``` | ||
|
||
Classes may be open for external inheritance as well. For that, use `openclass` instead of `classdef`. | ||
3. `interfacedef`: declares an interface that may be implemented by other types. Example: | ||
```kotlin | ||
val Interface = interfacedef { | ||
call("foo", PredefinedType.void, PredefinedType.void) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure it works. Because we don't support fields for interfaces. How is |
||
} | ||
val BaseClassWithInterface = baseclass implements Interface with { | ||
} | ||
val BaseStructWithInterface = basestruct implements Interface with { | ||
} | ||
``` | ||
|
||
Note that you can chain multiple `implements` calls if you need one type to implement several interfaces: | ||
```kotlin | ||
val DerivedClassWith2Interfaces = baseclass extends BaseClass implements Interface implements Interface2 with { | ||
field("derivedField", PredefinedType.bool) | ||
} | ||
``` | ||
4. `enum` types: | ||
```kotlin | ||
val MyEnum = enum { | ||
+"zero" | ||
+"two" | ||
+"three" | ||
+"five" | ||
+"six" | ||
} | ||
``` | ||
|
||
There are also certain predefined types inside of the `PredefinedType` object, such as `bool`, `int`, `string`, `guid`, `timeSpan` and so on, that are normally mapped to standard types of the corresponding languages during the generation stage. | ||
|
||
Also, collection types may be used, see `array(type)` or `immutableList(type)` for collection type definitions. | ||
|
||
#### Nullability | ||
To mark a type as nullable, mark add `nullable` after it. For example: | ||
|
||
```kotlin | ||
property("foo1", PredefinedType.int.nullable) | ||
``` | ||
|
||
This will have a language-specific effect on the generated code. | ||
|
||
#### Attributes | ||
It is possible to add certain attributes to entity types. For now, there are three attributes supported: `Nls`, `NlsSafe`, `NonNls`. All three affect localization, and are only emitted for Kotlin code right now. | ||
|
||
They are mapped to the following Java annotations: | ||
- `Nls` → `org.jetbrains.annotations.Nls` | ||
- `NonNls` → `org.jetbrains.annotations.NonNls` | ||
- `NlsSafe` → `com.intellij.openapi.util.NlsSafe` (IntelliJ-specific, should not be used outside of IntelliJ ecosystem) | ||
|
||
Also, there are extension members defined in `com.jetbrains.rd.generator.nova` to declare common string types: | ||
- `nlsString` | ||
- `nonNlsString` | ||
- `nlsSafeString` | ||
|
||
For other cases, you can use the attributes directly via `.attrs()` call. For example, to declare a nullable localizeable string: | ||
``` | ||
field("nls_nullable_field", PredefinedType.string.nullable.attrs(KnownAttrs.Nls)) | ||
``` | ||
|
||
#### Interning | ||
It's possible to reduce protocol traffic by enabling interning of certain objects. In the model definition, you can call `.interned(scope)` on any scalar definition, and that will start interning the values. The interned values are only sent the first time they appear in a scope, and in future appearances, only their id is sent. | ||
|
||
`scope` is the scope in that you want the values to be interned. For globally-interned values, use global `ProtocolInternScope`. Additionally, you can declare an intern scope in any top-level definition. | ||
|
||
For each field, the closest scope in the model hierarchy will be chosen and used for interning. If there's no such intern scope, then interning will be automatically disabled. | ||
|
||
Some examples: | ||
|
||
```kotlin | ||
map("issues", PredefinedType.int, structdef("ProtocolWrappedStringModel") { | ||
// These strings will be interned for the lifetime of the whole protocol: | ||
field("text", PredefinedType.string.interned(ProtocolInternScope)) | ||
}) | ||
|
||
object MyExample : Root() { | ||
val TestInternScope = internScope() | ||
|
||
internRoot(TestInternScope) | ||
val InterningTestModel = classdef { | ||
internRoot(TestInternScope) | ||
|
||
map("issues", PredefinedType.int, structdef("WrappedStringModel") { | ||
// These values will live as long as the parent class lives. | ||
field("text", PredefinedType.string.interned(TestInternScope)) | ||
}) | ||
} | ||
|
||
val InterningNestedTestModel = structdef { | ||
// These values will live for as long as the protocol root. | ||
field("inner", this.interned(TestInternScope).nullable) | ||
} | ||
} | ||
``` | ||
|
||
### Members | ||
Model types may contain a variety of members: | ||
- constants: `const("name", type` | ||
- read-only fields: `field("name", type, value)` | ||
- signals (bi-directional): `signal("name", signalValueType)` | ||
- sources (the signalling side of a signal): `source("name", signalValueType)` | ||
- sinks (the receiving side of a signal): `sink("name", signalValueType)` | ||
Comment on lines
+210
to
+211
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this correct? Check. |
||
- calls (for RPC, awaitable, the calling side): `call("name", argumentType, returnValueType)` | ||
- callbacks (the receiving side of the call): `callback("name", argumentType, returnValueType)` | ||
- properties: `property("name", valueType)` | ||
- async properties (properties that use the new threading model, see below): `asyncProperty("name", valueType)` | ||
- reactive collections: | ||
- lists: `list("name", itemType)` | ||
- sets: `set("name", itemType)` | ||
- async sets (the ones that use the new threading model, see below): `asyncSet("name", itemType)` | ||
- maps: `map("name", keyType, valueType)` | ||
- async maps (the ones that use the new threading model, see below): `asyncMap("name", keyType, valueType)` | ||
|
||
Note that the value type may only be scalar for async map. | ||
- TODO: `array` and below | ||
- methods (for interfaces): `method("name", returnType, vararg argumentTypes)` | ||
|
||
### Settings | ||
TODO | ||
|
||
#### Transformations (as-is, reversed, symmetric) | ||
TODO | ||
|
||
### Threading | ||
TODO: default threading model, `.async`, new threading model | ||
|
||
### Contexts | ||
TODO | ||
|
||
Gradle Configuration | ||
-------------------- | ||
TODO | ||
|
||
Examples | ||
-------- | ||
TODO (add links) | ||
|
||
Walkthrough | ||
----------- | ||
TODO |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe it would be better to use statful instead of persistent? Because persistent is associated with immutable collections or permanently stored on disk.