json-schema-to-typescript
Version:
compile json schema to typescript typings
764 lines (717 loc) • 27.2 kB
text/typescript
import {JSONSchema4Type, JSONSchema4TypeName} from 'json-schema'
import {includes, isPlainObject, map, omit} from 'lodash'
import {format} from 'util'
import {Options} from './'
import {applySchemaTyping} from './applySchemaTyping'
import type {AST, TInterface, TInterfaceParam, TIntersection, TNamedInterface, TTuple} from './types/AST'
import {
hasStandaloneName,
T_ANY,
T_ANY_ADDITIONAL_PROPERTIES,
T_UNKNOWN,
T_UNKNOWN_ADDITIONAL_PROPERTIES,
} from './types/AST'
import type {
EnumJSONSchema,
JSONSchemaWithDefinitions,
LinkedJSONSchema,
NormalizedJSONSchema,
SchemaSchema,
SchemaType,
} from './types/JSONSchema'
import {Intersection, Types, getRootSchema, isBoolean, isPrimitive} from './types/JSONSchema'
import {memoize} from './memoize'
import {DereferencedPaths} from './resolver'
import {generateName, justName, log, maybeStripDefault} from './utils'
export type Processed = Map<NormalizedJSONSchema, Map<SchemaType, AST>>
export type UsedNames = Set<string>
export function parse(
schema: NormalizedJSONSchema | JSONSchema4Type,
options: Options,
keyName?: string,
processed: Processed = new Map(),
usedNames = new Set<string>(),
): AST {
if (isPrimitive(schema)) {
if (isBoolean(schema)) {
return parseBooleanSchema(schema, keyName, options)
}
return parseLiteral(schema, keyName)
}
const intersection = schema[Intersection]
const types = schema[Types]
if (intersection) {
const ast = parseAsTypeWithCache(intersection, 'ALL_OF', options, keyName, processed, usedNames) as TIntersection
types.forEach(type => {
ast.params.push(parseAsTypeWithCache(schema, type, options, keyName, processed, usedNames))
})
log('blue', 'parser', 'Types:', [...types], 'Input:', schema, 'Output:', ast)
return ast
}
if (types.size === 1) {
const type = [...types][0]
const ast = parseAsTypeWithCache(schema, type, options, keyName, processed, usedNames)
log('blue', 'parser', 'Type:', type, 'Input:', schema, 'Output:', ast)
return ast
}
throw new ReferenceError('Expected intersection schema. Please file an issue on GitHub.')
}
/**
* A schema that refers back to itself can only be emitted as a named declaration:
* an anonymous type has no way to mention itself, so the generator would try to
* inline it forever. Recursive schemas usually have a name by the time they get
* here (a `title`, an `$id`, a key in `definitions`), but not always -- eg. a
* self-referencing `oneOf` that lives outside of `definitions`, or the copy the
* resolver makes of a definition wherever a `$ref` to it carries sibling keywords.
*
* Guarantees that every reference cycle in the AST passes through a named type, by
* naming anonymous types where it has to: after the `$ref` they were dereferenced
* from when there is one, else after the closest property key or `$ref` above them.
*/
export function nameAnonymousRecursiveTypes(
ast: AST,
processed: Processed,
dereferencedPaths: DereferencedPaths,
usedNames: UsedNames,
): void {
const refNames = new Map<AST, string>()
processed.forEach((asts, schema) => {
const name = justName(dereferencedPaths.get(schema))
if (name) {
asts.forEach(_ => refNames.set(_, name))
}
})
// The generator emits a named type by reference (and declares it separately), and
// inlines everything else -- so it recurses forever exactly when some cycle is made
// of anonymous types only. Walk the AST the same way: a named type ends the current
// path and becomes a root of its own, so an edge that leads back into the current
// path (past its root) closes an all-anonymous cycle. Name one of that edge's two
// ends; every cycle through the edge contains both. A type named part-way through a
// walk can hide a second cycle that shares its path but not that type, so walk
// again until a walk finds nothing to name (one extra walk, in practice).
let named: boolean
do {
named = false
const done = new Set<AST>()
const roots = [ast]
const path: AST[] = []
const visit = (node: AST): void => {
if (done.has(node)) {
return
}
if (path.length && hasStandaloneName(node)) {
roots.push(node)
return
}
const index = path.indexOf(node)
if (index > -1) {
if (!path.slice(index).some(hasStandaloneName)) {
const target = pickEnd(node, path[path.length - 1])
target.standaloneName = generateName(refNames.get(target) ?? keyOf(target) ?? closestName(path), usedNames)
named = true
}
return
}
path.push(node)
subtrees(node).forEach(visit)
path.pop()
done.add(node)
}
while (roots.length) {
visit(roots.pop()!)
}
} while (named)
// Prefer the end that was reached through a `$ref` (the resolver's copies share
// their children with the original, so a cycle through a copy is often entered at
// a child rather than at the copy), then the end that isn't a list, so that the
// alias reads `type Foo = string | Foo[]` rather than naming the array.
function pickEnd(node: AST, source: AST): AST {
if (refNames.has(node) !== refNames.has(source)) {
return refNames.has(node) ? node : source
}
return isList(node) && !isList(source) ? source : node
}
// Last resort is the root the walk started from: the schema itself or a named type
function closestName(path: AST[]): string {
const above = [...path].reverse()
return above.map(_ => refNames.get(_)).find(Boolean) ?? above.map(keyOf).find(Boolean) ?? path[0].standaloneName!
}
}
/** A node's property key -- ignoring the placeholder that array items get */
function keyOf(ast: AST): string | undefined {
return ast.keyName?.includes('{keyNameFromDefinition}') ? undefined : ast.keyName
}
function isList(ast: AST): boolean {
return ast.type === 'ARRAY' || ast.type === 'TUPLE'
}
function subtrees(ast: AST): AST[] {
switch (ast.type) {
case 'ARRAY':
return [ast.params]
case 'INTERFACE':
return ast.params.map(_ => _.ast).concat(ast.superTypes)
case 'INTERSECTION':
case 'UNION':
return ast.params
case 'TUPLE':
return ast.spreadParam ? ast.params.concat(ast.spreadParam) : ast.params
default:
return []
}
}
function parseAsTypeWithCache(
schema: NormalizedJSONSchema,
type: SchemaType,
options: Options,
keyName?: string,
processed: Processed = new Map(),
usedNames = new Set<string>(),
): AST {
// If we've seen this node before, return it.
let cachedTypeMap = processed.get(schema)
if (!cachedTypeMap) {
cachedTypeMap = new Map()
processed.set(schema, cachedTypeMap)
}
const cachedAST = cachedTypeMap.get(type)
if (cachedAST) {
return cachedAST
}
// Cache processed ASTs before they are actually computed, then update
// them in place using set(). This is to avoid cycles.
// TODO: Investigate alternative approaches (lazy-computing nodes, etc.)
const ast = {} as AST
cachedTypeMap.set(type, ast)
// Update the AST in place. This updates the `processed` cache, as well
// as any nodes that directly reference the node.
return Object.assign(ast, parseNonLiteral(schema, type, options, keyName, processed, usedNames))
}
function parseBooleanSchema(schema: boolean, keyName: string | undefined, options: Options): AST {
if (schema) {
return {
keyName,
type: options.unknownAny ? 'UNKNOWN' : 'ANY',
}
}
return {
keyName,
type: 'NEVER',
}
}
function parseLiteral(schema: JSONSchema4Type, keyName: string | undefined): AST {
return {
keyName,
params: schema,
type: 'LITERAL',
}
}
function parseNonLiteral(
schema: NormalizedJSONSchema,
type: SchemaType,
options: Options,
keyName: string | undefined,
processed: Processed,
usedNames: UsedNames,
): AST {
const definitions = getDefinitionsMemoized(getRootSchema(schema as any)) // TODO
const keyNameFromDefinition = getDefinitionKeysMemoized(definitions).get(schema)
switch (type) {
case 'ALL_OF':
return {
comment: schema.description,
deprecated: schema.deprecated,
keyName,
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options),
// An `allOf` member made up entirely of keywords this tool doesn't implement (eg.
// `if`/`then`/`else`, `not`) doesn't match any of the type matchers in `typesOfSchema`,
// so it falls back to `newInterface`, which synthesizes a bare `{[k: string]: unknown}`
// for it. Intersecting with that contributes no information, so drop it rather than
// cluttering the output. Restricted to members with no keyword this tool does recognize,
// so it never touches a member whose emptiness is due to its *own* type (eg. a bare
// `{type: 'object'}`, or `{required: [...]}` with no matching `properties`) -- those stay
// exactly as before.
params: schema
.allOf!.map(memberSchema => ({
ast: parse(memberSchema, options, undefined, processed, usedNames),
memberSchema,
}))
.filter(({ast, memberSchema}) => !(hasNoRecognizedKeywords(memberSchema) && isVacuousInterface(ast)))
.map(({ast}) => ast),
type: 'INTERSECTION',
}
case 'ANY':
return {
...(options.unknownAny ? T_UNKNOWN : T_ANY),
comment: schema.description,
deprecated: schema.deprecated,
keyName,
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options),
}
case 'ANY_OF':
return {
comment: schema.description,
deprecated: schema.deprecated,
keyName,
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options),
params: schema.anyOf!.map(_ => parse(_, options, undefined, processed, usedNames)),
type: 'UNION',
}
case 'BOOLEAN':
return {
comment: schema.description,
deprecated: schema.deprecated,
keyName,
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options),
type: 'BOOLEAN',
}
case 'CUSTOM_TYPE':
return {
comment: schema.description,
deprecated: schema.deprecated,
keyName,
params: schema.tsType!,
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options),
type: 'CUSTOM_TYPE',
}
case 'NAMED_ENUM': {
const enumName = standaloneName(schema, keyNameFromDefinition ?? keyName, usedNames, options)
// A TypeScript enum declaration requires a name. In positions that supply
// none (an `anyOf`/`oneOf` branch, say) fall back to a union of literals
// rather than emitting a nameless `export enum { ... }`, which is invalid.
if (!enumName) {
return {
comment: schema.description,
deprecated: schema.deprecated,
keyName,
params: (schema as EnumJSONSchema).enum!.map(_ => parseLiteral(_, undefined)),
type: 'UNION',
}
}
return {
comment: schema.description,
deprecated: schema.deprecated,
keyName,
standaloneName: enumName,
params: (schema as EnumJSONSchema).enum!.map((_, n) => ({
ast: parseLiteral(_, undefined),
keyName: schema.tsEnumNames![n],
})),
type: 'ENUM',
}
}
case 'NAMED_SCHEMA':
return newInterface(schema as SchemaSchema, options, processed, usedNames, keyName)
case 'NEVER':
return {
comment: schema.description,
deprecated: schema.deprecated,
keyName,
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options),
type: 'NEVER',
}
case 'NULL':
return {
comment: schema.description,
deprecated: schema.deprecated,
keyName,
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options),
type: 'NULL',
}
case 'NUMBER':
return {
comment: schema.description,
deprecated: schema.deprecated,
keyName,
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options),
type: 'NUMBER',
}
case 'OBJECT':
return {
comment: schema.description,
keyName,
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options),
type: 'OBJECT',
deprecated: schema.deprecated,
}
case 'ONE_OF':
return {
comment: schema.description,
deprecated: schema.deprecated,
keyName,
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options),
params: schema.oneOf!.map(_ => parse(_, options, undefined, processed, usedNames)),
type: 'UNION',
}
case 'REFERENCE':
throw Error(format('Refs should have been resolved by the resolver!', schema))
case 'STRING':
return {
comment: schema.description,
deprecated: schema.deprecated,
keyName,
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options),
type: 'STRING',
}
case 'TYPED_ARRAY':
if (Array.isArray(schema.items)) {
// normalised to not be undefined
const minItems = schema.minItems!
const maxItems = schema.maxItems!
const arrayType: TTuple = {
comment: schema.description,
deprecated: schema.deprecated,
keyName,
maxItems,
minItems,
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options),
params: schema.items.map(_ => parse(_, options, undefined, processed, usedNames)),
type: 'TUPLE',
}
if (schema.additionalItems === true) {
arrayType.spreadParam = options.unknownAny ? T_UNKNOWN : T_ANY
} else if (schema.additionalItems) {
arrayType.spreadParam = parse(schema.additionalItems, options, undefined, processed, usedNames)
}
return arrayType
} else {
return {
comment: schema.description,
deprecated: schema.deprecated,
keyName,
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options),
params: parse(schema.items!, options, `{keyNameFromDefinition}Items`, processed, usedNames),
type: 'ARRAY',
}
}
case 'UNION':
return {
comment: schema.description,
deprecated: schema.deprecated,
keyName,
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options),
params: (schema.type as JSONSchema4TypeName[]).map(type => {
const member: LinkedJSONSchema = {...omit(schema, '$id', 'description', 'title'), type}
maybeStripDefault(member)
applySchemaTyping(member)
return parse(member, options, undefined, processed, usedNames)
}),
type: 'UNION',
}
case 'UNNAMED_ENUM':
return {
comment: schema.description,
deprecated: schema.deprecated,
keyName,
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options),
params: (schema as EnumJSONSchema).enum!.map(_ => parseLiteral(_, undefined)),
type: 'UNION',
}
case 'UNNAMED_SCHEMA':
return newInterface(schema as SchemaSchema, options, processed, usedNames, keyName, keyNameFromDefinition)
case 'UNTYPED_ARRAY':
// normalised to not be undefined
const minItems = schema.minItems!
const maxItems = typeof schema.maxItems === 'number' ? schema.maxItems : -1
const params = options.unknownAny ? T_UNKNOWN : T_ANY
if (minItems > 0 || maxItems >= 0) {
return {
comment: schema.description,
deprecated: schema.deprecated,
keyName,
maxItems: schema.maxItems,
minItems,
// create a tuple of length N
params: Array(Math.max(maxItems, minItems) || 0).fill(params),
// if there is no maximum, then add a spread item to collect the rest
spreadParam: maxItems >= 0 ? undefined : params,
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options),
type: 'TUPLE',
}
}
return {
comment: schema.description,
deprecated: schema.deprecated,
keyName,
params,
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options),
type: 'ARRAY',
}
}
}
// Keywords that some matcher in `typesOfSchema`, or the `additionalProperties`/`required`
// normalizer rules, actually keys off of. An `allOf` member made up exclusively of keywords
// outside this list (eg. `if`/`then`/`else`, `not`) is one this tool has no notion of at all,
// as opposed to eg. a bare `{type: 'object'}`, which the tool does recognize but currently
// renders no differently -- that distinction keeps `hasNoRecognizedKeywords` from also
// swallowing members whose current (separately unimplemented) behavior other schemas rely on.
// (`$ref` is deliberately omitted: by the time this runs, the resolver has already replaced
// every `$ref` node, so `case 'REFERENCE'` above never fires and no schema here can carry one.)
// Keep this in sync with the keywords `typesOfSchema.ts`'s matchers check.
const RECOGNIZED_ALL_OF_MEMBER_KEYWORDS = new Set([
'$id',
'additionalProperties',
'allOf',
'anyOf',
'const',
'default',
'enum',
'extends',
'items',
'oneOf',
'patternProperties',
'properties',
'required',
'tsEnumNames',
'tsType',
'type',
])
function hasNoRecognizedKeywords(schema: NormalizedJSONSchema): boolean {
return Object.keys(schema).every(key => !RECOGNIZED_ALL_OF_MEMBER_KEYWORDS.has(key))
}
/**
* True for a parsed AST that carries no information beyond the synthesized
* `[k: string]: unknown`/`any` index signature `parseSchema` adds by default -- ie. an interface
* with no properties, patternProperties, superTypes, comment, or standalone name of its own.
* @see https://github.com/bcherny/json-schema-to-typescript/issues/369
*/
function isVacuousInterface(ast: AST): boolean {
return (
ast.type === 'INTERFACE' &&
ast.standaloneName === undefined &&
ast.comment === undefined &&
!ast.deprecated &&
ast.superTypes.length === 0 &&
ast.params.length === 1 &&
ast.params[0].isIndexSignature &&
(ast.params[0].ast.type === 'ANY' || ast.params[0].ast.type === 'UNKNOWN')
)
}
/**
* Compute a schema name using a series of fallbacks
*/
function standaloneName(
schema: NormalizedJSONSchema,
keyNameFromDefinition: string | undefined,
usedNames: UsedNames,
options: Options,
): string | undefined {
const name =
options.customName?.(schema, keyNameFromDefinition) || schema.title || schema.$id || keyNameFromDefinition
if (name) {
return generateName(name, usedNames)
}
}
function newInterface(
schema: SchemaSchema,
options: Options,
processed: Processed,
usedNames: UsedNames,
keyName?: string,
keyNameFromDefinition?: string,
): TInterface {
const name = standaloneName(schema, keyNameFromDefinition, usedNames, options)!
return {
comment: schema.description,
deprecated: schema.deprecated,
keyName,
params: parseSchema(schema, options, processed, usedNames, name),
standaloneName: name,
superTypes: parseSuperTypes(schema, options, processed, usedNames),
type: 'INTERFACE',
}
}
function parseSuperTypes(
schema: SchemaSchema,
options: Options,
processed: Processed,
usedNames: UsedNames,
): TNamedInterface[] {
// Type assertion needed because of dereferencing step
// TODO: Type it upstream
const superTypes = schema.extends as SchemaSchema[] | undefined
if (!superTypes) {
return []
}
return superTypes.map(_ => parse(_, options, undefined, processed, usedNames) as TNamedInterface)
}
/**
* Draft 4+ lists an object's required properties on the object schema (`required: [...]`).
* Draft 3 instead flagged each property schema (`required: true`), and some generators still
* emit that form. Support both, reading the draft 3 form only when it is strictly `true` so
* that a property's own `required` array (which of *its* properties are required) is never
* mistaken for the flag.
*/
function isRequired(parentSchema: SchemaSchema, key: string, propertySchema: NormalizedJSONSchema): boolean {
return propertySchema.required === true || (parentSchema.required !== true && includes(parentSchema.required, key))
}
/**
* Helper to parse schema properties into params on the parent schema's type
*/
function parseSchema(
schema: SchemaSchema,
options: Options,
processed: Processed,
usedNames: UsedNames,
parentSchemaName: string,
): TInterfaceParam[] {
const asts: TInterfaceParam[] = map(schema.properties, (value, key: string) => ({
ast: parse(value, options, key, processed, usedNames),
isIndexSignature: false,
isPatternProperty: false,
isRequired: isRequired(schema, key, value),
isUnreachableDefinition: false,
keyName: key,
}))
// rendered through the index signature (below), not as params of their own
const patternProperties: TInterfaceParam[] = map(schema.patternProperties, (value, key: string) => {
const ast = parse(value, options, key, processed, usedNames)
const comment = `This interface was referenced by \`${parentSchemaName}\`'s JSON-Schema definition
via the \`patternProperty\` "${key.replace('*/', '*\\/')}".`
ast.comment = ast.comment ? `${ast.comment}\n\n${comment}` : comment
return {
ast,
isIndexSignature: false,
isPatternProperty: true,
isRequired: isRequired(schema, key, value),
isUnreachableDefinition: false,
keyName: key,
}
})
const unreachableDefinitions: TInterfaceParam[] = !options.unreachableDefinitions
? []
: map(schema.$defs, (value, key: string) => {
const ast = parse(value, options, key, processed, usedNames)
const comment = `This interface was referenced by \`${parentSchemaName}\`'s JSON-Schema
via the \`definition\` "${key}".`
ast.comment = ast.comment ? `${ast.comment}\n\n${comment}` : comment
ast.isUnreachableDefinition = true
return {
ast,
isIndexSignature: false,
isPatternProperty: false,
isRequired: isRequired(schema, key, value),
isUnreachableDefinition: true,
keyName: key,
}
})
// TypeScript cannot constrain keys by regex, so patternProperties are folded into the one
// string index signature, typed as the union of their value types:
let declaredOnly: TInterfaceParam[] = [] // listed only so that their named types get declared
let indexSignatureMembers: TInterfaceParam[]
switch (schema.additionalProperties) {
case true: // already admits every value; the patterns are listed only to get their named types declared
declaredOnly = patternProperties
indexSignatureMembers = []
break
case undefined: // validate against the patterns alone, as if it were `false`
case false:
indexSignatureMembers = patternProperties
break
default:
indexSignatureMembers = patternProperties.concat({
ast: parse(schema.additionalProperties, options, '[k: string]', processed, usedNames),
isIndexSignature: false,
isPatternProperty: true,
isRequired: false,
isUnreachableDefinition: false,
keyName: '[k: string]',
})
}
if (!indexSignatureMembers.length && schema.additionalProperties === false) {
return asts.concat(unreachableDefinitions)
}
const members = indexSignatureMembers.map(_ => _.ast)
let indexSignature: AST
if (!members.length) {
indexSignature = options.unknownAny ? T_UNKNOWN_ADDITIONAL_PROPERTIES : T_ANY_ADDITIONAL_PROPERTIES
} else if (members.length === 1) {
indexSignature = members[0]
} else {
indexSignature = {
// Members with a standalone name carry their comment on their own declaration;
// the others' comments (which name their pattern) go on the index signature.
comment:
members
.filter(_ => !hasStandaloneName(_) && _.comment)
.map(_ => _.comment)
.join('\n\n') || undefined,
keyName: '[k: string]',
type: 'UNION',
params: members,
}
}
// pass "true" for isRequired because in TS, properties
// defined via index signatures are already optional
const indexSignatureParam: TInterfaceParam = {
ast: indexSignature,
isIndexSignature: true,
isPatternProperty: false,
isRequired: true,
isUnreachableDefinition: false,
keyName: '[k: string]',
}
// The members of a union are also listed as non-rendered params, so that their named types are
// still declared when the optimizer collapses it (e.g. `X | unknown` to `unknown`). They go
// after the index signature: the optimizer rewrites only the first param that holds a given AST.
if (indexSignatureMembers.length > 1) {
declaredOnly = indexSignatureMembers
}
// Declaration order as on master: types from patternProperties before unreachable definitions,
// a signature that comes from additionalProperties alone after them.
return patternProperties.length
? asts.concat(indexSignatureParam, declaredOnly, unreachableDefinitions)
: asts.concat(unreachableDefinitions, indexSignatureParam)
}
type Definitions = {[k: string]: NormalizedJSONSchema}
function getDefinitions(
schema: NormalizedJSONSchema,
isSchema = true,
processed = new Set<NormalizedJSONSchema>(),
): Definitions {
if (processed.has(schema)) {
return {}
}
processed.add(schema)
if (Array.isArray(schema)) {
return schema.reduce(
(prev, cur) => ({
...prev,
...getDefinitions(cur, false, processed),
}),
{},
)
}
if (isPlainObject(schema)) {
return {
...(isSchema && hasDefinitions(schema) ? schema.$defs : {}),
...Object.keys(schema).reduce<Definitions>(
(prev, cur) => ({
...prev,
...getDefinitions(schema[cur], false, processed),
}),
{},
),
}
}
return {}
}
const getDefinitionsMemoized = memoize(getDefinitions)
/**
* Reverse index of `getDefinitions`: schema -> the first definition key that holds it,
* built once per definitions object instead of scanning every key for every parsed node.
*/
const getDefinitionKeysMemoized = memoize((definitions: Definitions): Map<NormalizedJSONSchema, string> => {
const keys = new Map<NormalizedJSONSchema, string>()
for (const key of Object.keys(definitions)) {
if (!keys.has(definitions[key])) {
keys.set(definitions[key], key)
}
}
return keys
})
/**
* TODO: Reduce rate of false positives
*/
function hasDefinitions(schema: NormalizedJSONSchema): schema is JSONSchemaWithDefinitions {
return '$defs' in schema
}