Skip to content

Commit 207728b

Browse files
committed
feat: support non-object model types for rust
Those model types are common when using GET parameters in an OpenAPI document. Typescript already has a TypeRenderer. This adds an equivalent for Rust. However, instead of using a Rust type aliases, this PR uses the New Type Idiom. There are two main advantages to using the New Type idiom: compile-time value type validation and a bypass of the rust orphan rule (see https://effective-rust.com/newtype.html). The second advantage will be needed to implement model validation. Likely this validation will come from an external trait. Rust only allows adding external trait implementations to types that are internal to the crate (type aliases do not count).
1 parent f46affe commit 207728b

File tree

14 files changed

+519
-1
lines changed

14 files changed

+519
-1
lines changed

docs/presets.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Modelina uses something called **presets** to extend the rendered model. You can
3737
- [**Package**](#package)
3838
- [**Union**](#union)
3939
- [**Tuple**](#tuple)
40+
- [**NewType**](#newtype)
4041
+ [Dart](#dart)
4142
- [**Class**](#class-4)
4243
- [**Enum**](#enum-5)
@@ -496,6 +497,17 @@ This preset is a generator for the meta model `ConstrainedTupleModel` and [can b
496497
| `field` | | `field` object as a [`ConstrainedTupleValueModel`](./internal-model.md#the-constrained-meta-model) instance, `fieldIndex`. |
497498
| `structMacro` | | `field` object as a [`ConstrainedTupleValueModel`](./internal-model.md#the-constrained-meta-model) instance, `fieldIndex`. |
498499

500+
#### **NewType**
501+
502+
This preset is a generator for the meta model `ConstrainedMetaModel` and [can be accessed through the `model` argument](#presets-shape).
503+
This preset is called each time the [New Type pattern](https://doc.rust-lang.org/rust-by-example/generics/new_types.html) is used to wrap
504+
non-object models (i.e., boolean, integers, floats, strings or arrays).
505+
506+
| Method | Description | Additional arguments |
507+
|---|---|---|
508+
| `field` | A method to extend rendered the only field: the wrapped model. | |
509+
| `structMacro` | | |
510+
499511
### Dart
500512
#### **Class**
501513

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ These are all the basic generator examples that shows a bare minimal example of
7373
- [generate-csharp-models](./generate-csharp-models) - A basic example to generate C# data models
7474
- [generate-python-models](./generate-python-models) - A basic example showing how to generate Python models.
7575
- [rust-generate-crate](./rust-generate-crate) - A basic example showing how to generate a Rust package.
76+
- [rust-generate-newtype-idiom](./rust-generate-newtype-idiom) - A basic example of how Modelina generates a new type for each non-object types: integers, floats, booleans, strings and arrays.
7677
- [generate-java-models](./generate-java-models) - A basic example to generate Java data models.
7778
- [generate-go-models](./generate-go-models) - A basic example to generate Go data models
7879
- [generate-javascript-models](./generate-javascript-models) - A basic example to generate JavaScript data models
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
output
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Rust Data Models for non-object types
2+
3+
A basic example of how Modelina generates a new type for each non-object types: integers, floats, booleans, strings and arrays.
4+
Modelina leverages Rust's [NewType Idiom approach](https://doc.rust-lang.org/rust-by-example/generics/new_types.html).
5+
6+
## Requirements
7+
8+
- [Rust](https://rustup.rs/)
9+
10+
Rust is required to compile this example.
11+
12+
## How to run this example
13+
14+
Run this example using:
15+
16+
```sh
17+
npm i && npm run start
18+
```
19+
20+
If you are on Windows, use the `start:windows` script instead:
21+
22+
```sh
23+
npm i && npm run start:windows
24+
```
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`Should be able to render Rust Models and should log expected output to console 1`] = `
4+
Array [
5+
"// BooleanReturn represents a BooleanReturn model.
6+
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
7+
pub struct BooleanReturn(pub bool);
8+
9+
",
10+
]
11+
`;
12+
13+
exports[`Should be able to render Rust Models and should log expected output to console 2`] = `
14+
Array [
15+
"// ArrayReturn represents a ArrayReturn model.
16+
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
17+
pub struct ArrayReturn(pub Vec<String>);
18+
19+
",
20+
]
21+
`;
22+
23+
exports[`Should be able to render Rust Models and should log expected output to console 3`] = `
24+
Array [
25+
"// IntegerParam represents a IntegerParam model.
26+
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
27+
pub struct IntegerParam(pub i64);
28+
29+
",
30+
]
31+
`;
32+
33+
exports[`Should be able to render Rust Models and should log expected output to console 4`] = `
34+
Array [
35+
"// FloatParam represents a FloatParam model.
36+
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, PartialOrd, Serialize)]
37+
pub struct FloatParam(pub f64);
38+
39+
",
40+
]
41+
`;
42+
43+
exports[`Should be able to render Rust Models and should log expected output to console 5`] = `
44+
Array [
45+
"// BooleanParam represents a BooleanParam model.
46+
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
47+
pub struct BooleanParam(pub bool);
48+
49+
",
50+
]
51+
`;
52+
53+
exports[`Should be able to render Rust Models and should log expected output to console 6`] = `
54+
Array [
55+
"// StringParam represents a StringParam model.
56+
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
57+
pub struct StringParam(pub String);
58+
59+
",
60+
]
61+
`;
62+
63+
exports[`Should be able to render Rust Models and should log expected output to console 7`] = `
64+
Array [
65+
"[package]
66+
name = \\"newtype-idiom-rs-example\\"
67+
version = \\"1.0.0\\"
68+
authors = [\\"AsyncAPI Rust Champions\\"]
69+
homepage = \\"https://www.asyncapi.com/tools/modelina\\"
70+
repository = \\"https://github.com/asyncapi/modelina\\"
71+
license = \\"Apache-2.0\\"
72+
description = \\"Rust models generated by AsyncAPI Modelina\\"
73+
edition = \\"2018\\"
74+
75+
[dependencies]
76+
serde = { version = \\"1\\", features = [\\"derive\\"] }
77+
serde_json = { version=\\"1\\", optional = true }
78+
79+
[dev-dependencies]
80+
81+
[features]
82+
default = [\\"json\\"]
83+
json = [\\"dep:serde_json\\"]",
84+
]
85+
`;
86+
87+
exports[`Should be able to render Rust Models and should log expected output to console 8`] = `
88+
Array [
89+
"#[macro_use]
90+
extern crate serde;
91+
extern crate serde_json;
92+
93+
pub mod boolean_return;
94+
pub use self::boolean_return::*;
95+
96+
pub mod array_return;
97+
pub use self::array_return::*;
98+
99+
pub mod integer_param;
100+
pub use self::integer_param::*;
101+
102+
pub mod float_param;
103+
pub use self::float_param::*;
104+
105+
pub mod boolean_param;
106+
pub use self::boolean_param::*;
107+
108+
pub mod string_param;
109+
pub use self::string_param::*;",
110+
]
111+
`;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
const spy = jest.spyOn(global.console, 'log').mockImplementation(() => {
2+
return;
3+
});
4+
import { generate } from './index';
5+
describe('Should be able to render Rust Models', () => {
6+
afterAll(() => {
7+
jest.restoreAllMocks();
8+
});
9+
test('and should log expected output to console', async () => {
10+
await generate();
11+
expect(spy.mock.calls.length).toEqual(8);
12+
expect(spy.mock.calls[0]).toMatchSnapshot();
13+
expect(spy.mock.calls[1]).toMatchSnapshot();
14+
expect(spy.mock.calls[2]).toMatchSnapshot();
15+
expect(spy.mock.calls[3]).toMatchSnapshot();
16+
expect(spy.mock.calls[4]).toMatchSnapshot();
17+
expect(spy.mock.calls[5]).toMatchSnapshot();
18+
expect(spy.mock.calls[6]).toMatchSnapshot();
19+
expect(spy.mock.calls[7]).toMatchSnapshot();
20+
});
21+
});
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import {
2+
RustFileGenerator,
3+
RustRenderCompleteModelOptions,
4+
RUST_COMMON_PRESET,
5+
defaultRustRenderCompleteModelOptions,
6+
RustPackageFeatures
7+
} from '../../src/generators';
8+
import * as path from 'path';
9+
10+
const openAPIDocument = {
11+
openapi: '3.1.0',
12+
info: {
13+
version: '0.1',
14+
title:
15+
'A basic example of how Modelina generates a new type for each non-object types: integers, floats, booleans, strings and arrays.'
16+
},
17+
paths: {
18+
'/example': {
19+
get: {
20+
parameters: [
21+
{
22+
in: 'path',
23+
name: 'integerParam',
24+
schema: {
25+
type: 'integer',
26+
format: 'int64',
27+
title: 'integerParam'
28+
},
29+
required: true,
30+
description: 'An example of integer parameter'
31+
},
32+
{
33+
in: 'path',
34+
name: 'floatParam',
35+
schema: {
36+
type: 'number',
37+
title: 'floatParam'
38+
},
39+
required: true,
40+
description: 'An example of float parameter'
41+
},
42+
{
43+
in: 'path',
44+
name: 'booleanParam',
45+
schema: {
46+
type: 'boolean',
47+
title: 'booleanParam'
48+
},
49+
required: true,
50+
description: 'An example of boolean parameter'
51+
},
52+
{
53+
in: 'path',
54+
name: 'stringParam',
55+
schema: {
56+
type: 'string',
57+
title: 'stringParam'
58+
},
59+
required: true,
60+
description: 'An example of string parameter'
61+
}
62+
],
63+
responses: {
64+
200: {
65+
content: {
66+
'application/json': {
67+
schema: {
68+
type: 'boolean',
69+
title: 'booleanReturn'
70+
}
71+
}
72+
}
73+
},
74+
500: {
75+
content: {
76+
'application/json': {
77+
schema: {
78+
type: 'array',
79+
title: 'ArrayReturn',
80+
items: {
81+
type: 'string',
82+
title: 'StringReturn'
83+
}
84+
}
85+
}
86+
}
87+
}
88+
}
89+
}
90+
}
91+
}
92+
};
93+
94+
export async function generate(): Promise<void> {
95+
// initialize the generator from a preset
96+
const generator = new RustFileGenerator({
97+
presets: [
98+
{
99+
preset: RUST_COMMON_PRESET,
100+
options: {
101+
implementNew: true,
102+
implementDefault: true
103+
}
104+
}
105+
]
106+
});
107+
// Generated files will be written to output/ directory
108+
const outDir = path.join(__dirname, 'output');
109+
110+
// Run the file generator with options
111+
const models = await generator.generateToPackage(openAPIDocument, outDir, {
112+
...defaultRustRenderCompleteModelOptions,
113+
supportFiles: true, // generate Cargo.toml and lib.rs
114+
package: {
115+
packageName: 'newtype-idiom-rs-example',
116+
packageVersion: '1.0.0',
117+
// set authors, homepage, repository, and license
118+
authors: ['AsyncAPI Rust Champions'],
119+
homepage: 'https://www.asyncapi.com/tools/modelina',
120+
repository: 'https://github.com/asyncapi/modelina',
121+
license: 'Apache-2.0',
122+
description: 'Rust models generated by AsyncAPI Modelina',
123+
// support 2018 editions and up
124+
edition: '2018',
125+
// enable serde_json
126+
packageFeatures: [RustPackageFeatures.json] as RustPackageFeatures[]
127+
}
128+
} as RustRenderCompleteModelOptions);
129+
for (const model of models) {
130+
console.log(model.result);
131+
}
132+
}
133+
if (require.main === module) {
134+
generate();
135+
}

examples/rust-generate-newtype-idiom/package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"config": {
3+
"example_name": "rust-generate-newtype-idiom"
4+
},
5+
"scripts": {
6+
"install": "cd ../.. && npm i",
7+
"clean": "rm -rf output",
8+
"start": "../../node_modules/.bin/ts-node --cwd ../../ ./examples/$npm_package_config_example_name/index.ts generate && cargo build --manifest-path=output/Cargo.toml ",
9+
"start:windows": "..\\..\\node_modules\\.bin\\ts-node --cwd ..\\..\\ .\\examples\\%npm_package_config_example_name%\\index.ts",
10+
"test": "../../node_modules/.bin/jest --config=../../jest.config.js ./examples/$npm_package_config_example_name/index.spec.ts",
11+
"test:windows": "..\\..\\node_modules\\.bin\\jest --config=..\\..\\jest.config.js examples/%npm_package_config_example_name%/index.spec.ts"
12+
}
13+
}

0 commit comments

Comments
 (0)