Skip to content

Commit c89275f

Browse files
authored
feat: add support xml samples (#173)
1 parent 1b6adcb commit c89275f

File tree

9 files changed

+632
-5
lines changed

9 files changed

+632
-5
lines changed

package-lock.json

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"types": "./src/types.d.ts",
88
"scripts": {
99
"lint": "eslint .",
10+
"lint:fix": "eslint . --fix",
1011
"test": "npm run lint && jest",
1112
"test:watch": "jest --watch",
1213
"coverage": "jest --coverage",
@@ -67,6 +68,7 @@
6768
},
6869
"dependencies": {
6970
"@types/json-schema": "^7.0.7",
71+
"fast-xml-parser": "^4.5.0",
7072
"json-pointer": "0.6.2"
7173
},
7274
"overrides": {

src/openapi-sampler.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { traverse, clearCache } from './traverse';
22
import { sampleArray, sampleBoolean, sampleNumber, sampleObject, sampleString } from './samplers/index';
3+
import { XMLBuilder } from 'fast-xml-parser';
34

45
export var _samplers = {};
56

@@ -8,10 +9,30 @@ const defaults = {
89
maxSampleDepth: 15,
910
};
1011

12+
function convertJsonToXml(obj, schema) {
13+
if (!obj) {
14+
throw new Error('Unknown format output for building XML.');
15+
}
16+
if (Array.isArray(obj) || Object.keys(obj).length > 1) {
17+
obj = { [schema?.xml?.name || 'root']: obj }; // XML document must contain one root element
18+
}
19+
const builder = new XMLBuilder({
20+
ignoreAttributes : false,
21+
format: true,
22+
attributeNamePrefix: '$',
23+
textNodeName: '#text',
24+
});
25+
return builder.build(obj);
26+
}
27+
1128
export function sample(schema, options, spec) {
1229
let opts = Object.assign({}, defaults, options);
1330
clearCache();
14-
return traverse(schema, opts, spec).value;
31+
let result = traverse(schema, opts, spec).value;
32+
if (opts?.format === 'xml') {
33+
return convertJsonToXml(result, schema);
34+
}
35+
return result;
1536
};
1637

1738
export function _registerSampler(type, sampler) {

src/samplers/array.js

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { traverse } from '../traverse';
2+
import { applyXMLAttributes } from '../utils';
3+
24
export function sampleArray(schema, options = {}, spec, context) {
35
const depth = (context && context.depth || 1);
46

@@ -22,7 +24,30 @@ export function sampleArray(schema, options = {}, spec, context) {
2224
for (let i = 0; i < arrayLength; i++) {
2325
let itemSchema = itemSchemaGetter(i);
2426
let { value: sample } = traverse(itemSchema, options, spec, {depth: depth + 1});
25-
res.push(sample);
27+
if (options?.format === 'xml') {
28+
const { value, propertyName } = applyXMLAttributes({value: sample}, itemSchema, context);
29+
if (propertyName) {
30+
if (!res?.[propertyName]) {
31+
res = { ...res, [propertyName]: [] };
32+
}
33+
res[propertyName].push(value);
34+
} else {
35+
res = {...res, ...value};
36+
}
37+
} else {
38+
res.push(sample);
39+
}
40+
}
41+
42+
if (options?.format === 'xml' && depth === 1) {
43+
const { value, propertyName } = applyXMLAttributes({value: null}, schema, context);
44+
if (propertyName) {
45+
if (value) {
46+
res = Array.isArray(res) ? { [propertyName]: {...value, ...res.map(item => ({['#text']: {...item}}))} } : { [propertyName]: {...res, ...value }};
47+
} else {
48+
res = { [propertyName]: res };
49+
}
50+
}
2651
}
2752
return res;
2853
}

src/samplers/object.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { traverse } from '../traverse';
2+
import { applyXMLAttributes } from '../utils';
3+
24
export function sampleObject(schema, options = {}, spec, context) {
35
let res = {};
46
const depth = (context && context.depth || 1);
@@ -27,7 +29,17 @@ export function sampleObject(schema, options = {}, spec, context) {
2729
if (options.skipWriteOnly && sample.writeOnly) {
2830
return;
2931
}
30-
res[propertyName] = sample.value;
32+
33+
if (options?.format === 'xml') {
34+
const { propertyName: newPropertyName, value } = applyXMLAttributes(sample, schema.properties[propertyName], { propertyName });
35+
if (newPropertyName) {
36+
res[newPropertyName] = value;
37+
} else {
38+
res = { ...res, ...value };
39+
}
40+
} else {
41+
res[propertyName] = sample.value;
42+
}
3143
});
3244
}
3345

src/traverse.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { allOfSample } from './allOf';
33
import { inferType } from './infer';
44
import { getResultForCircular, mergeDeep, popSchemaStack } from './utils';
55
import JsonPointer from 'json-pointer';
6+
import { applyXMLAttributes } from './utils';
67

78
let $refCache = {};
89
// for circular JS references we use additional array and not object as we need to compare entire schemas and not strings
@@ -69,7 +70,14 @@ export function traverse(schema, options, spec, context) {
6970

7071
if ($refCache[ref] !== true) {
7172
$refCache[ref] = true;
72-
result = traverse(referenced, options, spec, context);
73+
const traverseResult = traverse(referenced, options, spec, context);
74+
if (options.format === 'xml') {
75+
const {propertyName, value} = applyXMLAttributes(traverseResult, referenced, context);
76+
result = {...traverseResult, value: {[propertyName || 'root']: value}};
77+
} else {
78+
result = traverseResult;
79+
}
80+
7381
$refCache[ref] = false;
7482
} else {
7583
const referencedType = inferType(referenced);

src/types.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ export interface Options {
55
readonly skipReadOnly?: boolean;
66
readonly skipWriteOnly?: boolean;
77
readonly quiet?: boolean;
8-
readonly enablePatterns?: boolean
8+
readonly enablePatterns?: boolean;
9+
readonly format?: 'json' | 'xml';
910
}
1011

1112
export function sample(schema: JSONSchema7, options?: Options, document?: object): unknown;

src/utils.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,63 @@ export function popSchemaStack(seenSchemasStack, context) {
7171
if (context) seenSchemasStack.pop();
7272
}
7373

74+
export function getXMLAttributes(schema) {
75+
return {
76+
name: schema?.xml?.name || '',
77+
prefix: schema?.xml?.prefix || '',
78+
namespace: schema?.xml?.namespace || null,
79+
attribute: schema?.xml?.attribute ?? false,
80+
wrapped: schema?.xml?.wrapped ?? false,
81+
};
82+
}
83+
84+
export function applyXMLAttributes(result, schema = {}, context = {}) {
85+
const { value: oldValue } = result;
86+
const { propertyName: oldPropertyName } = context;
87+
const { name, prefix, namespace, attribute, wrapped } =
88+
getXMLAttributes(schema);
89+
let propertyName = name || oldPropertyName ? `${prefix ? prefix + ':' : ''}${name || oldPropertyName}` : null;
90+
91+
let value = typeof oldValue === 'object'
92+
? Array.isArray(oldValue)
93+
? [...oldValue]
94+
: { ...oldValue }
95+
: oldValue;
96+
97+
if (attribute && propertyName) {
98+
propertyName = `$${propertyName}`;
99+
}
100+
101+
if (namespace) {
102+
if (typeof value === 'object') {
103+
value[`$xmlns${prefix ? ':' + prefix : ''}`] = namespace;
104+
} else {
105+
value = { [`$xmlns${prefix ? ':' + prefix : ''}`]: namespace, ['#text']: value };
106+
}
107+
}
108+
109+
if (schema.type === 'array') {
110+
if (wrapped && Array.isArray(value)) {
111+
value = { [propertyName]: [...value] };
112+
}
113+
if (!wrapped) {
114+
propertyName = null;
115+
}
116+
117+
if (schema.example !== undefined && !wrapped) {
118+
propertyName = schema.items.xml?.name || propertyName;
119+
}
120+
}
121+
if (schema.oneOf || schema.anyOf || schema.allOf || schema.$ref) {
122+
propertyName = null;
123+
}
124+
125+
return {
126+
propertyName,
127+
value,
128+
};
129+
}
130+
74131
function hashCode(str) {
75132
var hash = 0;
76133
if (str.length == 0) return hash;

0 commit comments

Comments
 (0)