@typra/emitter 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/cleanup/generated-file.d.ts +6 -0
- package/dist/src/cleanup/generated-file.js +61 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +110 -0
- package/dist/src/decorators.d.ts +56 -0
- package/dist/src/decorators.js +177 -0
- package/dist/src/emitter.d.ts +13 -0
- package/dist/src/emitter.js +137 -0
- package/dist/src/generate.d.ts +86 -0
- package/dist/src/generate.js +104 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +5 -0
- package/dist/src/ir/ast.d.ts +235 -0
- package/dist/src/ir/ast.js +589 -0
- package/dist/src/ir/declarations.d.ts +364 -0
- package/dist/src/ir/declarations.js +23 -0
- package/dist/src/ir/expansion.d.ts +140 -0
- package/dist/src/ir/expansion.js +407 -0
- package/dist/src/ir/lower.d.ts +53 -0
- package/dist/src/ir/lower.js +480 -0
- package/dist/src/ir/utilities.d.ts +12 -0
- package/dist/src/ir/utilities.js +39 -0
- package/dist/src/ir/visitor.d.ts +29 -0
- package/dist/src/ir/visitor.js +48 -0
- package/dist/src/languages/csharp/driver.d.ts +5 -0
- package/dist/src/languages/csharp/driver.js +315 -0
- package/dist/src/languages/csharp/emitter.d.ts +33 -0
- package/dist/src/languages/csharp/emitter.js +1140 -0
- package/dist/src/languages/csharp/scaffolding.d.ts +18 -0
- package/dist/src/languages/csharp/scaffolding.js +591 -0
- package/dist/src/languages/csharp/test-emitter.d.ts +43 -0
- package/dist/src/languages/csharp/test-emitter.js +274 -0
- package/dist/src/languages/csharp/visitor.d.ts +14 -0
- package/dist/src/languages/csharp/visitor.js +79 -0
- package/dist/src/languages/go/driver.d.ts +12 -0
- package/dist/src/languages/go/driver.js +128 -0
- package/dist/src/languages/go/emitter.d.ts +33 -0
- package/dist/src/languages/go/emitter.js +879 -0
- package/dist/src/languages/go/scaffolding.d.ts +18 -0
- package/dist/src/languages/go/scaffolding.js +53 -0
- package/dist/src/languages/go/test-emitter.d.ts +20 -0
- package/dist/src/languages/go/test-emitter.js +300 -0
- package/dist/src/languages/go/visitor.d.ts +14 -0
- package/dist/src/languages/go/visitor.js +78 -0
- package/dist/src/languages/markdown/driver.d.ts +19 -0
- package/dist/src/languages/markdown/driver.js +408 -0
- package/dist/src/languages/python/driver.d.ts +14 -0
- package/dist/src/languages/python/driver.js +372 -0
- package/dist/src/languages/python/emitter.d.ts +31 -0
- package/dist/src/languages/python/emitter.js +856 -0
- package/dist/src/languages/python/scaffolding.d.ts +33 -0
- package/dist/src/languages/python/scaffolding.js +279 -0
- package/dist/src/languages/python/test-emitter.d.ts +29 -0
- package/dist/src/languages/python/test-emitter.js +388 -0
- package/dist/src/languages/python/visitor.d.ts +14 -0
- package/dist/src/languages/python/visitor.js +65 -0
- package/dist/src/languages/rust/driver.d.ts +13 -0
- package/dist/src/languages/rust/driver.js +624 -0
- package/dist/src/languages/rust/emitter.d.ts +45 -0
- package/dist/src/languages/rust/emitter.js +1596 -0
- package/dist/src/languages/rust/visitor.d.ts +25 -0
- package/dist/src/languages/rust/visitor.js +153 -0
- package/dist/src/languages/typescript/driver.d.ts +8 -0
- package/dist/src/languages/typescript/driver.js +209 -0
- package/dist/src/languages/typescript/emitter.d.ts +42 -0
- package/dist/src/languages/typescript/emitter.js +904 -0
- package/dist/src/languages/typescript/scaffolding.d.ts +32 -0
- package/dist/src/languages/typescript/scaffolding.js +303 -0
- package/dist/src/languages/typescript/test-emitter.d.ts +23 -0
- package/dist/src/languages/typescript/test-emitter.js +204 -0
- package/dist/src/languages/typescript/visitor.d.ts +14 -0
- package/dist/src/languages/typescript/visitor.js +64 -0
- package/dist/src/lib.d.ts +33 -0
- package/dist/src/lib.js +101 -0
- package/dist/src/testing/index.d.ts +2 -0
- package/dist/src/testing/index.js +8 -0
- package/dist/src/testing/test-context.d.ts +63 -0
- package/dist/src/testing/test-context.js +355 -0
- package/fixtures/shapes/main.tsp +43 -0
- package/fixtures/tspconfig.yaml +13 -0
- package/package.json +76 -0
- package/src/lib/main.tsp +110 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
|
+
import { execFileSync } from "child_process";
|
|
4
|
+
import { existsSync, mkdirSync, writeFileSync, unlinkSync } from "fs";
|
|
5
|
+
import * as YAML from "yaml";
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
/**
|
|
9
|
+
* Generate Typra runtime surfaces.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { generate } from '@typra/emitter/generate';
|
|
14
|
+
*
|
|
15
|
+
* await generate({
|
|
16
|
+
* output: './generated',
|
|
17
|
+
* targets: ['python', 'csharp'],
|
|
18
|
+
* rootObject: 'Typra.Widget',
|
|
19
|
+
* omit: ['LegacyWidget']
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export async function generate(options) {
|
|
24
|
+
const { output, targets = ["python", "csharp", "typescript", "go"], rootObject = "Typra.FixtureRoot", omit = [], namespace = "Typra", rootAlias, generateTests = true, format = true, } = options;
|
|
25
|
+
// Resolve the model path (inside the package)
|
|
26
|
+
// __dirname is dist/src at runtime, so we need to go up two levels to package root
|
|
27
|
+
const packageRoot = path.resolve(__dirname, "../..");
|
|
28
|
+
const modelPath = path.resolve(packageRoot, "lib/model/main.tsp");
|
|
29
|
+
// Ensure output directory exists
|
|
30
|
+
const outputDir = path.resolve(output);
|
|
31
|
+
if (!existsSync(outputDir)) {
|
|
32
|
+
mkdirSync(outputDir, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
// Build emit targets configuration
|
|
35
|
+
const emitTargets = buildEmitTargets(targets, outputDir, generateTests, format);
|
|
36
|
+
// Create temporary tspconfig.yaml
|
|
37
|
+
const tspConfig = {
|
|
38
|
+
emit: ["@typra/emitter"],
|
|
39
|
+
options: {
|
|
40
|
+
"@typra/emitter": {
|
|
41
|
+
"emitter-output-dir": outputDir,
|
|
42
|
+
"root-object": rootObject,
|
|
43
|
+
"root-namespace": namespace,
|
|
44
|
+
...(rootAlias && { "root-alias": rootAlias }),
|
|
45
|
+
...(omit.length > 0 && { "omit-models": omit }),
|
|
46
|
+
"emit-targets": emitTargets,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
// Write temporary config file
|
|
51
|
+
const tempConfigPath = path.join(outputDir, ".tspconfig.temp.yaml");
|
|
52
|
+
writeFileSync(tempConfigPath, YAML.stringify(tspConfig));
|
|
53
|
+
try {
|
|
54
|
+
// Run tsp compile — use execFileSync to avoid shell injection
|
|
55
|
+
execFileSync("tsp", ["compile", modelPath, "--config", tempConfigPath], {
|
|
56
|
+
stdio: "inherit",
|
|
57
|
+
cwd: outputDir,
|
|
58
|
+
});
|
|
59
|
+
return {
|
|
60
|
+
success: true,
|
|
61
|
+
outputDir,
|
|
62
|
+
targets: Array.isArray(targets) ? targets : Object.keys(targets),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
return {
|
|
67
|
+
success: false,
|
|
68
|
+
outputDir,
|
|
69
|
+
targets: Array.isArray(targets) ? targets : Object.keys(targets),
|
|
70
|
+
errors: [error instanceof Error ? error.message : String(error)],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
// Clean up temp config
|
|
75
|
+
try {
|
|
76
|
+
unlinkSync(tempConfigPath);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Ignore cleanup errors
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function buildEmitTargets(targets, baseOutput, generateTests, format) {
|
|
84
|
+
if (Array.isArray(targets)) {
|
|
85
|
+
// Simple array of target names - use default directories
|
|
86
|
+
return targets.map(target => ({
|
|
87
|
+
type: target,
|
|
88
|
+
"output-dir": path.join(baseOutput, target),
|
|
89
|
+
"test-dir": generateTests ? path.join(baseOutput, target, "tests") : undefined,
|
|
90
|
+
format,
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
// Object with per-target configuration
|
|
95
|
+
return Object.entries(targets).map(([target, opts]) => ({
|
|
96
|
+
type: target,
|
|
97
|
+
"output-dir": opts.outputDir,
|
|
98
|
+
"test-dir": opts.testDir,
|
|
99
|
+
format: opts.format ?? format,
|
|
100
|
+
namespace: opts.namespace,
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=generate.js.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { $onEmit, GeneratorOptions, filterNodes } from "./emitter.js";
|
|
2
|
+
export { $lib } from "./lib.js";
|
|
3
|
+
export { $sample, $abstract, $coerce, $factory, $method, $knownAs, $defaultFor, $protocol } from "./decorators.js";
|
|
4
|
+
export { generate, GenerateOptions, GenerateResult, TargetLanguage, TargetOptions } from "./generate.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { $onEmit, filterNodes } from "./emitter.js";
|
|
2
|
+
export { $lib } from "./lib.js";
|
|
3
|
+
export { $sample, $abstract, $coerce, $factory, $method, $knownAs, $defaultFor, $protocol } from "./decorators.js";
|
|
4
|
+
export { generate } from "./generate.js";
|
|
5
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { Type, Model, Scalar, Union, Program, ModelProperty } from "@typespec/compiler";
|
|
2
|
+
import { SampleEntry, FactoryEntry, MethodEntry, KnownAsEntry, DefaultForEntry } from "../decorators.js";
|
|
3
|
+
export interface TypeName {
|
|
4
|
+
namespace: string;
|
|
5
|
+
name: string;
|
|
6
|
+
}
|
|
7
|
+
export interface Coercion {
|
|
8
|
+
scalar: string;
|
|
9
|
+
expansion: {
|
|
10
|
+
[key: string]: any;
|
|
11
|
+
};
|
|
12
|
+
example?: any;
|
|
13
|
+
title?: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare class TypeNode {
|
|
17
|
+
model: Model;
|
|
18
|
+
typeName: TypeName;
|
|
19
|
+
description: string;
|
|
20
|
+
base: TypeName | null;
|
|
21
|
+
childTypes: TypeNode[];
|
|
22
|
+
coercions: Coercion[];
|
|
23
|
+
properties: PropertyNode[];
|
|
24
|
+
isAbstract: boolean;
|
|
25
|
+
isProtocol: boolean;
|
|
26
|
+
discriminator: string | undefined;
|
|
27
|
+
factories: FactoryEntry[];
|
|
28
|
+
methods: MethodEntry[];
|
|
29
|
+
/** Semantic group derived from the TSP source subfolder (e.g. "connection", "tools"). */
|
|
30
|
+
group: string;
|
|
31
|
+
constructor(model: Model, description: string);
|
|
32
|
+
retrievePolymorphicTypes(): any;
|
|
33
|
+
getSanitizedObject(): Record<string, any>;
|
|
34
|
+
}
|
|
35
|
+
export declare class PropertyNode {
|
|
36
|
+
name: string;
|
|
37
|
+
typeName: TypeName;
|
|
38
|
+
description: string;
|
|
39
|
+
samples: SampleEntry[];
|
|
40
|
+
knownAs: KnownAsEntry[];
|
|
41
|
+
defaultFor: DefaultForEntry[];
|
|
42
|
+
isScalar: boolean;
|
|
43
|
+
isOptional: boolean;
|
|
44
|
+
isCollection: boolean;
|
|
45
|
+
isAny: boolean;
|
|
46
|
+
isDict: boolean;
|
|
47
|
+
defaultValue: string | number | boolean | null;
|
|
48
|
+
allowedValues: string[];
|
|
49
|
+
/** Name of the string-literal union alias (e.g., "Role"), null if unnamed or not an enum. */
|
|
50
|
+
enumName: string | null;
|
|
51
|
+
/** True when the union includes a bare `string` variant (open enum — accepts any string). */
|
|
52
|
+
isOpenEnum: boolean;
|
|
53
|
+
property: ModelProperty;
|
|
54
|
+
type: TypeNode | undefined;
|
|
55
|
+
constructor(property: ModelProperty, description: string);
|
|
56
|
+
getSanitizedObject(): Record<string, any>;
|
|
57
|
+
}
|
|
58
|
+
export declare const enumerateTypes: (node: TypeNode, visited?: Set<string>) => IterableIterator<TypeNode>;
|
|
59
|
+
export declare const resolveModel: (program: Program, model: Model, visited: Set<string> | undefined, rootNamespace: string, rootAlias: string) => TypeNode;
|
|
60
|
+
export declare const resolveModelChildren: (program: Program, model: Model, visited: Set<string>, rootNamespace: string, rootAlias: string) => TypeNode[];
|
|
61
|
+
export declare const resolveProperty: (program: Program, property: ModelProperty, visited: Set<string>, rootNamespace: string, rootAlias: string) => PropertyNode;
|
|
62
|
+
export declare const resolveScalarProperty: (program: Program, property: ModelProperty, scalar: Scalar) => PropertyNode;
|
|
63
|
+
export declare const resolveIntrinsicProperty: (program: Program, property: ModelProperty, intrinsic: Type, visited: Set<string>) => PropertyNode;
|
|
64
|
+
export declare const resolveModelProperty: (program: Program, property: ModelProperty, model: Model, visited: Set<string>, rootNamespace: string, rootAlias: string) => PropertyNode;
|
|
65
|
+
export declare const resolveUnionProperty: (program: Program, property: ModelProperty, union: Union, visited: Set<string>, rootNamespace: string, rootAlias: string) => PropertyNode;
|
|
66
|
+
/**
|
|
67
|
+
* Context for rendering a single Python class.
|
|
68
|
+
*/
|
|
69
|
+
export interface PythonClassContext {
|
|
70
|
+
/** The TypeNode being rendered */
|
|
71
|
+
node: TypeNode;
|
|
72
|
+
/** Type mapping from TypeSpec types to Python types */
|
|
73
|
+
typeMapper: Record<string, string>;
|
|
74
|
+
/** Processed coercion representations for scalar-to-object constructors */
|
|
75
|
+
coercions: Array<{
|
|
76
|
+
scalar: string;
|
|
77
|
+
alternate: string;
|
|
78
|
+
}>;
|
|
79
|
+
/** Polymorphic type information if this is a discriminated type */
|
|
80
|
+
polymorphicTypes: ReturnType<TypeNode['retrievePolymorphicTypes']> | undefined;
|
|
81
|
+
/** Import types needed from other modules */
|
|
82
|
+
imports: string[];
|
|
83
|
+
/** Collection properties with their nested type info for load_* methods */
|
|
84
|
+
collectionTypes: Array<{
|
|
85
|
+
prop: PropertyNode;
|
|
86
|
+
type: string[];
|
|
87
|
+
}>;
|
|
88
|
+
/** The property name that receives the scalar value in a coercion expansion */
|
|
89
|
+
coercionProperty: string | null;
|
|
90
|
+
/** Maps factory.name → safe Python method name (prefixed with create_ on field collision) */
|
|
91
|
+
factoryNameMap: Record<string, string>;
|
|
92
|
+
/** Pre-rendered factory method bodies via expression IR */
|
|
93
|
+
renderedFactories: Array<{
|
|
94
|
+
name: string;
|
|
95
|
+
safeName: string;
|
|
96
|
+
params: Record<string, string>;
|
|
97
|
+
body: string;
|
|
98
|
+
}>;
|
|
99
|
+
/** Pre-rendered coercion expressions via expression IR */
|
|
100
|
+
renderedCoercions: Array<{
|
|
101
|
+
scalar: string;
|
|
102
|
+
expression: string;
|
|
103
|
+
}>;
|
|
104
|
+
/** Type names referenced in factory expressions (for file-level import resolution) */
|
|
105
|
+
factoryTypeRefs: string[];
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Context for rendering a Python file containing one or more classes.
|
|
109
|
+
*/
|
|
110
|
+
export interface PythonFileContext {
|
|
111
|
+
/** Whether any class in the file is abstract */
|
|
112
|
+
containsAbstract: boolean;
|
|
113
|
+
/** Python typing imports needed (e.g., "Any", "Callable", "Optional") */
|
|
114
|
+
typings: string[];
|
|
115
|
+
/** Grouped imports: each entry maps a module name to the types imported from it */
|
|
116
|
+
imports: Array<{
|
|
117
|
+
module: string;
|
|
118
|
+
names: string[];
|
|
119
|
+
}>;
|
|
120
|
+
/** Array of class contexts to render */
|
|
121
|
+
classes: PythonClassContext[];
|
|
122
|
+
/** Type mapping from TypeSpec types to Python types */
|
|
123
|
+
typeMapper: Record<string, string>;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Context for rendering a Python __init__.py file.
|
|
127
|
+
*/
|
|
128
|
+
export interface PythonInitContext {
|
|
129
|
+
/** Base types (types without a parent) for top-level imports */
|
|
130
|
+
baseTypes: TypeNode[];
|
|
131
|
+
/** All types for __all__ export list */
|
|
132
|
+
types: TypeNode[];
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Context for rendering Python test files.
|
|
136
|
+
*/
|
|
137
|
+
export interface PythonTestContext {
|
|
138
|
+
/** The TypeNode being tested */
|
|
139
|
+
node: TypeNode;
|
|
140
|
+
/** Flattened sample combinations for testing */
|
|
141
|
+
examples: Array<{
|
|
142
|
+
json: string[];
|
|
143
|
+
yaml: string[];
|
|
144
|
+
validation: Array<{
|
|
145
|
+
key: string;
|
|
146
|
+
value: any;
|
|
147
|
+
delimeter: string;
|
|
148
|
+
}>;
|
|
149
|
+
}>;
|
|
150
|
+
/** Coercion representation tests */
|
|
151
|
+
coercions: Array<{
|
|
152
|
+
title: string;
|
|
153
|
+
scalar: string;
|
|
154
|
+
value: string;
|
|
155
|
+
validation: Array<{
|
|
156
|
+
key: string;
|
|
157
|
+
value: any;
|
|
158
|
+
delimeter: string;
|
|
159
|
+
}>;
|
|
160
|
+
}>;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Context for rendering the LoadContext file.
|
|
164
|
+
*/
|
|
165
|
+
export interface PythonLoadContextContext {
|
|
166
|
+
/** File header comment */
|
|
167
|
+
header: string;
|
|
168
|
+
/** Package name for imports in test file */
|
|
169
|
+
package?: string;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Base render context interface - all language contexts should extend this.
|
|
173
|
+
* This ensures consistency across emitters.
|
|
174
|
+
*/
|
|
175
|
+
export interface BaseRenderContext {
|
|
176
|
+
/** The TypeNode being rendered */
|
|
177
|
+
node: TypeNode;
|
|
178
|
+
/** Type mapping from TypeSpec types to target language types */
|
|
179
|
+
typeMapper: Record<string, string>;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Validation assertion for a single property in a test.
|
|
183
|
+
*/
|
|
184
|
+
export interface PropertyValidation {
|
|
185
|
+
/** Property name in target language casing (PascalCase, snake_case, camelCase) */
|
|
186
|
+
key: string;
|
|
187
|
+
/** Expected value after loading */
|
|
188
|
+
value: any;
|
|
189
|
+
/** String delimiter for assertions (", """, etc.) */
|
|
190
|
+
delimiter: string;
|
|
191
|
+
/** Whether property is optional/pointer (for Go, C# nullable) */
|
|
192
|
+
isOptional: boolean;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* A single test example generated from @sample decorators.
|
|
196
|
+
*/
|
|
197
|
+
export interface TestExample {
|
|
198
|
+
/** JSON representation as lines */
|
|
199
|
+
json: string[];
|
|
200
|
+
/** YAML representation as lines */
|
|
201
|
+
yaml: string[];
|
|
202
|
+
/** Property assertions to validate after loading */
|
|
203
|
+
validations: PropertyValidation[];
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* A coercion (scalar-to-object) representation test case.
|
|
207
|
+
*/
|
|
208
|
+
export interface CoercionTest {
|
|
209
|
+
/** Human-readable test name/title */
|
|
210
|
+
title: string;
|
|
211
|
+
/** Scalar type name in target language */
|
|
212
|
+
scalarType: string;
|
|
213
|
+
/** Example scalar value as string literal */
|
|
214
|
+
value: string;
|
|
215
|
+
/** Validations after expansion to full object */
|
|
216
|
+
validations: PropertyValidation[];
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Base test context interface - all language test contexts should use this structure.
|
|
220
|
+
* This ensures consistency in test generation across all emitters.
|
|
221
|
+
*/
|
|
222
|
+
export interface BaseTestContext {
|
|
223
|
+
/** The TypeNode being tested */
|
|
224
|
+
node: TypeNode;
|
|
225
|
+
/** Whether this is an abstract/polymorphic base type (skip direct instantiation tests) */
|
|
226
|
+
isAbstract: boolean;
|
|
227
|
+
/** Package/namespace name for imports (optional - not used by all languages) */
|
|
228
|
+
package?: string;
|
|
229
|
+
/** Test examples from @sample decorators */
|
|
230
|
+
examples: TestExample[];
|
|
231
|
+
/** Coercion (scalar-to-object) representation tests */
|
|
232
|
+
coercions: CoercionTest[];
|
|
233
|
+
/** Factory methods declared via @factory (for auto-generated factory tests) */
|
|
234
|
+
factories: FactoryEntry[];
|
|
235
|
+
}
|