Skip to content

Commit

Permalink
feat: add support xml samples (#173)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexVarchuk authored Nov 20, 2024
1 parent 1b6adcb commit c89275f
Show file tree
Hide file tree
Showing 9 changed files with 632 additions and 5 deletions.
27 changes: 27 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"types": "./src/types.d.ts",
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"test": "npm run lint && jest",
"test:watch": "jest --watch",
"coverage": "jest --coverage",
Expand Down Expand Up @@ -67,6 +68,7 @@
},
"dependencies": {
"@types/json-schema": "^7.0.7",
"fast-xml-parser": "^4.5.0",
"json-pointer": "0.6.2"
},
"overrides": {
Expand Down
23 changes: 22 additions & 1 deletion src/openapi-sampler.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { traverse, clearCache } from './traverse';
import { sampleArray, sampleBoolean, sampleNumber, sampleObject, sampleString } from './samplers/index';
import { XMLBuilder } from 'fast-xml-parser';

export var _samplers = {};

Expand All @@ -8,10 +9,30 @@ const defaults = {
maxSampleDepth: 15,
};

function convertJsonToXml(obj, schema) {
if (!obj) {
throw new Error('Unknown format output for building XML.');
}
if (Array.isArray(obj) || Object.keys(obj).length > 1) {
obj = { [schema?.xml?.name || 'root']: obj }; // XML document must contain one root element
}
const builder = new XMLBuilder({
ignoreAttributes : false,
format: true,
attributeNamePrefix: '$',
textNodeName: '#text',
});
return builder.build(obj);
}

export function sample(schema, options, spec) {
let opts = Object.assign({}, defaults, options);
clearCache();
return traverse(schema, opts, spec).value;
let result = traverse(schema, opts, spec).value;
if (opts?.format === 'xml') {
return convertJsonToXml(result, schema);
}
return result;
};

export function _registerSampler(type, sampler) {
Expand Down
27 changes: 26 additions & 1 deletion src/samplers/array.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { traverse } from '../traverse';
import { applyXMLAttributes } from '../utils';

export function sampleArray(schema, options = {}, spec, context) {
const depth = (context && context.depth || 1);

Expand All @@ -22,7 +24,30 @@ export function sampleArray(schema, options = {}, spec, context) {
for (let i = 0; i < arrayLength; i++) {
let itemSchema = itemSchemaGetter(i);
let { value: sample } = traverse(itemSchema, options, spec, {depth: depth + 1});
res.push(sample);
if (options?.format === 'xml') {
const { value, propertyName } = applyXMLAttributes({value: sample}, itemSchema, context);
if (propertyName) {
if (!res?.[propertyName]) {
res = { ...res, [propertyName]: [] };
}
res[propertyName].push(value);
} else {
res = {...res, ...value};
}
} else {
res.push(sample);
}
}

if (options?.format === 'xml' && depth === 1) {
const { value, propertyName } = applyXMLAttributes({value: null}, schema, context);
if (propertyName) {
if (value) {
res = Array.isArray(res) ? { [propertyName]: {...value, ...res.map(item => ({['#text']: {...item}}))} } : { [propertyName]: {...res, ...value }};
} else {
res = { [propertyName]: res };
}
}
}
return res;
}
14 changes: 13 additions & 1 deletion src/samplers/object.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { traverse } from '../traverse';
import { applyXMLAttributes } from '../utils';

export function sampleObject(schema, options = {}, spec, context) {
let res = {};
const depth = (context && context.depth || 1);
Expand Down Expand Up @@ -27,7 +29,17 @@ export function sampleObject(schema, options = {}, spec, context) {
if (options.skipWriteOnly && sample.writeOnly) {
return;
}
res[propertyName] = sample.value;

if (options?.format === 'xml') {
const { propertyName: newPropertyName, value } = applyXMLAttributes(sample, schema.properties[propertyName], { propertyName });
if (newPropertyName) {
res[newPropertyName] = value;
} else {
res = { ...res, ...value };
}
} else {
res[propertyName] = sample.value;
}
});
}

Expand Down
10 changes: 9 additions & 1 deletion src/traverse.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { allOfSample } from './allOf';
import { inferType } from './infer';
import { getResultForCircular, mergeDeep, popSchemaStack } from './utils';
import JsonPointer from 'json-pointer';
import { applyXMLAttributes } from './utils';

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

if ($refCache[ref] !== true) {
$refCache[ref] = true;
result = traverse(referenced, options, spec, context);
const traverseResult = traverse(referenced, options, spec, context);
if (options.format === 'xml') {
const {propertyName, value} = applyXMLAttributes(traverseResult, referenced, context);
result = {...traverseResult, value: {[propertyName || 'root']: value}};
} else {
result = traverseResult;
}

$refCache[ref] = false;
} else {
const referencedType = inferType(referenced);
Expand Down
3 changes: 2 additions & 1 deletion src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ export interface Options {
readonly skipReadOnly?: boolean;
readonly skipWriteOnly?: boolean;
readonly quiet?: boolean;
readonly enablePatterns?: boolean
readonly enablePatterns?: boolean;
readonly format?: 'json' | 'xml';
}

export function sample(schema: JSONSchema7, options?: Options, document?: object): unknown;
57 changes: 57 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,63 @@ export function popSchemaStack(seenSchemasStack, context) {
if (context) seenSchemasStack.pop();
}

export function getXMLAttributes(schema) {
return {
name: schema?.xml?.name || '',
prefix: schema?.xml?.prefix || '',
namespace: schema?.xml?.namespace || null,
attribute: schema?.xml?.attribute ?? false,
wrapped: schema?.xml?.wrapped ?? false,
};
}

export function applyXMLAttributes(result, schema = {}, context = {}) {
const { value: oldValue } = result;
const { propertyName: oldPropertyName } = context;
const { name, prefix, namespace, attribute, wrapped } =
getXMLAttributes(schema);
let propertyName = name || oldPropertyName ? `${prefix ? prefix + ':' : ''}${name || oldPropertyName}` : null;

let value = typeof oldValue === 'object'
? Array.isArray(oldValue)
? [...oldValue]
: { ...oldValue }
: oldValue;

if (attribute && propertyName) {
propertyName = `$${propertyName}`;
}

if (namespace) {
if (typeof value === 'object') {
value[`$xmlns${prefix ? ':' + prefix : ''}`] = namespace;
} else {
value = { [`$xmlns${prefix ? ':' + prefix : ''}`]: namespace, ['#text']: value };
}
}

if (schema.type === 'array') {
if (wrapped && Array.isArray(value)) {
value = { [propertyName]: [...value] };
}
if (!wrapped) {
propertyName = null;
}

if (schema.example !== undefined && !wrapped) {
propertyName = schema.items.xml?.name || propertyName;
}
}
if (schema.oneOf || schema.anyOf || schema.allOf || schema.$ref) {
propertyName = null;
}

return {
propertyName,
value,
};
}

function hashCode(str) {
var hash = 0;
if (str.length == 0) return hash;
Expand Down
Loading

0 comments on commit c89275f

Please sign in to comment.