Skip to content

Commit 48b5d20

Browse files
committed
Customise oneOf handling
1 parent 8f809cc commit 48b5d20

File tree

2 files changed

+426
-0
lines changed

2 files changed

+426
-0
lines changed
Lines changed: 373 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,373 @@
1+
{{#models}}
2+
{{#model}}
3+
{{/model}}
4+
{{/models}}
5+
export * from './models';
6+
7+
{{#models}}
8+
{{#model}}
9+
import { {{classname}}{{#oneOf}}{{#-first}}Class{{/-first}}{{/oneOf}} } from './{{#lambda.camelcase}}{{ name }}{{/lambda.camelcase}}{{importFileExtension}}';
10+
{{/model}}
11+
{{/models}}
12+
13+
/* tslint:disable:no-unused-variable */
14+
let primitives = [
15+
"string",
16+
"boolean",
17+
"double",
18+
"integer",
19+
"long",
20+
"float",
21+
"number",
22+
"any"
23+
];
24+
25+
let enumsMap: Set<string> = new Set<string>([
26+
{{#models}}
27+
{{#model}}
28+
{{#isEnum}}
29+
"{{classname}}.{{enumName}}",
30+
{{/isEnum}}
31+
{{#hasEnums}}
32+
{{#vars}}
33+
{{#isEnum}}
34+
"{{classname}}.{{enumName}}",
35+
{{/isEnum}}
36+
{{/vars}}
37+
{{/hasEnums}}
38+
{{/model}}
39+
{{/models}}
40+
]);
41+
42+
let typeMap: {[index: string]: any} = {
43+
{{#models}}
44+
{{#model}}
45+
{{^isEnum}}
46+
"{{classname}}": {{classname}}{{#oneOf}}{{#-first}}Class{{/-first}}{{/oneOf}},
47+
{{/isEnum}}
48+
{{/model}}
49+
{{/models}}
50+
}
51+
52+
type MimeTypeDescriptor = {
53+
type: string;
54+
subtype: string;
55+
subtypeTokens: string[];
56+
};
57+
58+
/**
59+
* Every mime-type consists of a type, subtype, and optional parameters.
60+
* The subtype can be composite, including information about the content format.
61+
* For example: `application/json-patch+json`, `application/merge-patch+json`.
62+
*
63+
* This helper transforms a string mime-type into an internal representation.
64+
* This simplifies the implementation of predicates that in turn define common rules for parsing or stringifying
65+
* the payload.
66+
*/
67+
const parseMimeType = (mimeType: string): MimeTypeDescriptor => {
68+
const [type = '', subtype = ''] = mimeType.split('/');
69+
return {
70+
type,
71+
subtype,
72+
subtypeTokens: subtype.split('+'),
73+
};
74+
};
75+
76+
type MimeTypePredicate = (mimeType: string) => boolean;
77+
78+
// This factory creates a predicate function that checks a string mime-type against defined rules.
79+
const mimeTypePredicateFactory = (predicate: (descriptor: MimeTypeDescriptor) => boolean): MimeTypePredicate => (mimeType) => predicate(parseMimeType(mimeType));
80+
81+
// Use this factory when you need to define a simple predicate based only on type and, if applicable, subtype.
82+
const mimeTypeSimplePredicateFactory = (type: string, subtype?: string): MimeTypePredicate => mimeTypePredicateFactory((descriptor) => {
83+
if (descriptor.type !== type) return false;
84+
if (subtype != null && descriptor.subtype !== subtype) return false;
85+
return true;
86+
});
87+
88+
// Creating a set of named predicates that will help us determine how to handle different mime-types
89+
const isTextLikeMimeType = mimeTypeSimplePredicateFactory('text');
90+
const isJsonMimeType = mimeTypeSimplePredicateFactory('application', 'json');
91+
const isJsonLikeMimeType = mimeTypePredicateFactory((descriptor) => descriptor.type === 'application' && descriptor.subtypeTokens.some((item) => item === 'json'));
92+
const isOctetStreamMimeType = mimeTypeSimplePredicateFactory('application', 'octet-stream');
93+
const isFormUrlencodedMimeType = mimeTypeSimplePredicateFactory('application', 'x-www-form-urlencoded');
94+
95+
// Defining a list of mime-types in the order of prioritization for handling.
96+
const supportedMimeTypePredicatesWithPriority: MimeTypePredicate[] = [
97+
isJsonMimeType,
98+
isJsonLikeMimeType,
99+
isTextLikeMimeType,
100+
isOctetStreamMimeType,
101+
isFormUrlencodedMimeType,
102+
];
103+
104+
const nullableSuffix = " | null";
105+
const optionalSuffix = " | undefined";
106+
const arrayPrefix = "Array<";
107+
const arraySuffix = ">";
108+
const mapPrefix = "{ [key: string]: ";
109+
const mapSuffix = "; }";
110+
111+
export class ObjectSerializer {
112+
public static findCorrectType(data: any, expectedType: string) {
113+
if (data == undefined) {
114+
return expectedType;
115+
} else if (primitives.indexOf(expectedType.toLowerCase()) !== -1) {
116+
return expectedType;
117+
} else if (expectedType === "Date") {
118+
return expectedType;
119+
} else {
120+
if (enumsMap.has(expectedType)) {
121+
return expectedType;
122+
}
123+
124+
if (!typeMap[expectedType]) {
125+
return expectedType; // w/e we don't know the type
126+
}
127+
128+
// Check the discriminator
129+
let discriminatorProperty = typeMap[expectedType].discriminator;
130+
if (discriminatorProperty == null) {
131+
return expectedType; // the type does not have a discriminator. use it.
132+
} else {
133+
if (data[discriminatorProperty]) {
134+
var discriminatorType = data[discriminatorProperty];
135+
let mapping = typeMap[expectedType].mapping;
136+
if (mapping != undefined && mapping[discriminatorType]) {
137+
return mapping[discriminatorType]; // use the type given in the discriminator
138+
} else if(typeMap[discriminatorType]) {
139+
return discriminatorType;
140+
} else {
141+
return expectedType; // discriminator did not map to a type
142+
}
143+
} else {
144+
return expectedType; // discriminator was not present (or an empty string)
145+
}
146+
}
147+
}
148+
}
149+
150+
/**
151+
* Serializes a value into a plain JSON-compatible object based on its type.
152+
*
153+
* Supports primitives, arrays, maps, dates, enums, and classes defined in `typeMap`.
154+
* Falls back to raw data if type is unknown or lacks `getAttributeTypeMap()`.
155+
*
156+
* @param data - The value to serialize.
157+
* @param type - The expected type name as a string.
158+
* @param format - Format hint (e.g. "date" or "date-time").
159+
* @returns A JSON-compatible representation of `data`.
160+
*/
161+
public static serialize(data: any, type: string, format: string): any {
162+
if (data == undefined) {
163+
return data;
164+
} else if (primitives.indexOf(type.toLowerCase()) !== -1) {
165+
return data;
166+
} else if (type.endsWith(nullableSuffix)) {
167+
let subType: string = type.slice(0, -nullableSuffix.length); // Type | null => Type
168+
return ObjectSerializer.serialize(data, subType, format);
169+
} else if (type.endsWith(optionalSuffix)) {
170+
let subType: string = type.slice(0, -optionalSuffix.length); // Type | undefined => Type
171+
return ObjectSerializer.serialize(data, subType, format);
172+
} else if (type.startsWith(arrayPrefix)) {
173+
let subType: string = type.slice(arrayPrefix.length, -arraySuffix.length); // Array<Type> => Type
174+
let transformedData: any[] = [];
175+
for (let date of data) {
176+
transformedData.push(ObjectSerializer.serialize(date, subType, format));
177+
}
178+
return transformedData;
179+
} else if (type.startsWith(mapPrefix)) {
180+
let subType: string = type.slice(mapPrefix.length, -mapSuffix.length); // { [key: string]: Type; } => Type
181+
let transformedData: { [key: string]: any } = {};
182+
for (let key in data) {
183+
transformedData[key] = ObjectSerializer.serialize(
184+
data[key],
185+
subType,
186+
format,
187+
);
188+
}
189+
return transformedData;
190+
} else if (type === "Date") {
191+
if (format == "date") {
192+
let month = data.getMonth()+1
193+
month = month < 10 ? "0" + month.toString() : month.toString()
194+
let day = data.getDate();
195+
day = day < 10 ? "0" + day.toString() : day.toString();
196+
197+
return data.getFullYear() + "-" + month + "-" + day;
198+
} else {
199+
return data.toISOString();
200+
}
201+
} else {
202+
if (enumsMap.has(type)) {
203+
return data;
204+
}
205+
if (!typeMap[type]) { // in case we dont know the type
206+
return data;
207+
}
208+
209+
// Get the actual type of this object
210+
type = this.findCorrectType(data, type);
211+
212+
const clazz = typeMap[type];
213+
214+
// Safe check for getAttributeTypeMap
215+
if (typeof clazz.getAttributeTypeMap !== "function") {
216+
return { ...data }; // fallback: shallow copy
217+
}
218+
219+
// get the map for the correct type.
220+
let attributeTypes = typeMap[type].getAttributeTypeMap();
221+
let instance: {[index: string]: any} = {};
222+
for (let attributeType of attributeTypes) {
223+
instance[attributeType.baseName] = ObjectSerializer.serialize(data[attributeType.name], attributeType.type, attributeType.format);
224+
}
225+
return instance;
226+
}
227+
}
228+
229+
/**
230+
* Deserializes a plain JSON-compatible object into a typed instance.
231+
*
232+
* Handles primitives, arrays, maps, dates, enums, and known classes from `typeMap`.
233+
* Uses discriminators when available to resolve polymorphic types.
234+
* Falls back to raw data if the type is unknown or lacks `getAttributeTypeMap()`.
235+
*
236+
* @param data - The raw input to deserialize.
237+
* @param type - The expected type name as a string.
238+
* @param format - Format hint (e.g. "date" or "date-time").
239+
* @returns A deserialized instance or value of `data`.
240+
*/
241+
public static deserialize(data: any, type: string, format: string): any {
242+
// polymorphism may change the actual type.
243+
type = ObjectSerializer.findCorrectType(data, type);
244+
if (data == undefined) {
245+
return data;
246+
} else if (primitives.indexOf(type.toLowerCase()) !== -1) {
247+
return data;
248+
} else if (type.endsWith(nullableSuffix)) {
249+
let subType: string = type.slice(0, -nullableSuffix.length); // Type | null => Type
250+
return ObjectSerializer.deserialize(data, subType, format);
251+
} else if (type.endsWith(optionalSuffix)) {
252+
let subType: string = type.slice(0, -optionalSuffix.length); // Type | undefined => Type
253+
return ObjectSerializer.deserialize(data, subType, format);
254+
} else if (type.startsWith(arrayPrefix)) {
255+
let subType: string = type.slice(arrayPrefix.length, -arraySuffix.length); // Array<Type> => Type
256+
let transformedData: any[] = [];
257+
for (let date of data) {
258+
transformedData.push(ObjectSerializer.deserialize(date, subType, format));
259+
}
260+
return transformedData;
261+
} else if (type.startsWith(mapPrefix)) {
262+
let subType: string = type.slice(mapPrefix.length, -mapSuffix.length); // { [key: string]: Type; } => Type
263+
let transformedData: { [key: string]: any } = {};
264+
for (let key in data) {
265+
transformedData[key] = ObjectSerializer.deserialize(
266+
data[key],
267+
subType,
268+
format,
269+
);
270+
}
271+
return transformedData;
272+
} else if (type === "Date") {
273+
return new Date(data);
274+
} else {
275+
if (enumsMap.has(type)) {// is Enum
276+
return data;
277+
}
278+
279+
if (!typeMap[type]) { // dont know the type
280+
return data;
281+
}
282+
let instance = new typeMap[type]();
283+
284+
// Safe check for getAttributeTypeMap
285+
if (typeof typeMap[type].getAttributeTypeMap !== "function") {
286+
Object.assign(instance, data); // fallback: shallow copy
287+
return instance;
288+
}
289+
290+
let attributeTypes = typeMap[type].getAttributeTypeMap();
291+
for (let attributeType of attributeTypes) {
292+
let value = ObjectSerializer.deserialize(data[attributeType.baseName], attributeType.type, attributeType.format);
293+
if (value !== undefined) {
294+
instance[attributeType.name] = value;
295+
}
296+
}
297+
return instance;
298+
}
299+
}
300+
301+
302+
/**
303+
* Normalize media type
304+
*
305+
* We currently do not handle any media types attributes, i.e. anything
306+
* after a semicolon. All content is assumed to be UTF-8 compatible.
307+
*/
308+
public static normalizeMediaType(mediaType: string | undefined): string | undefined {
309+
if (mediaType === undefined) {
310+
return undefined;
311+
}
312+
return (mediaType.split(";")[0] ?? '').trim().toLowerCase();
313+
}
314+
315+
/**
316+
* From a list of possible media types, choose the one we can handle best.
317+
*
318+
* The order of the given media types does not have any impact on the choice
319+
* made.
320+
*/
321+
public static getPreferredMediaType(mediaTypes: Array<string>): string {
322+
/** According to OAS 3 we should default to json */
323+
if (mediaTypes.length === 0) {
324+
return "application/json";
325+
}
326+
327+
const normalMediaTypes = mediaTypes.map(ObjectSerializer.normalizeMediaType);
328+
329+
for (const predicate of supportedMimeTypePredicatesWithPriority) {
330+
for (const mediaType of normalMediaTypes) {
331+
if (mediaType != null && predicate(mediaType)) {
332+
return mediaType;
333+
}
334+
}
335+
}
336+
337+
throw new Error("None of the given media types are supported: " + mediaTypes.join(", "));
338+
}
339+
340+
/**
341+
* Convert data to a string according the given media type
342+
*/
343+
public static stringify(data: any, mediaType: string): string {
344+
if (isTextLikeMimeType(mediaType)) {
345+
return String(data);
346+
}
347+
348+
if (isJsonLikeMimeType(mediaType)) {
349+
return JSON.stringify(data);
350+
}
351+
352+
throw new Error("The mediaType " + mediaType + " is not supported by ObjectSerializer.stringify.");
353+
}
354+
355+
/**
356+
* Parse data from a string according to the given media type
357+
*/
358+
public static parse(rawData: string, mediaType: string | undefined) {
359+
if (mediaType === undefined) {
360+
throw new Error("Cannot parse content. No Content-Type defined.");
361+
}
362+
363+
if (isTextLikeMimeType(mediaType)) {
364+
return rawData;
365+
}
366+
367+
if (isJsonLikeMimeType(mediaType)) {
368+
return JSON.parse(rawData);
369+
}
370+
371+
throw new Error("The mediaType " + mediaType + " is not supported by ObjectSerializer.parse.");
372+
}
373+
}

0 commit comments

Comments
 (0)