Skip to content

Commit

Permalink
fix(execute): explode object parameters for combined schemas (#3782)
Browse files Browse the repository at this point in the history
Co-authored-by: Vladimír Gorej <vladimir.gorej@gmail.com>
  • Loading branch information
robert-hebel-sb and char0n authored Feb 18, 2025
1 parent 890e280 commit 1c968ca
Show file tree
Hide file tree
Showing 4 changed files with 244 additions and 13 deletions.
2 changes: 2 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ export const ACCEPT_HEADER_VALUE_FOR_DOCUMENTS = 'application/json, application/
export const DEFAULT_BASE_URL = 'https://swagger.io';

export const DEFAULT_OPENAPI_3_SERVER = Object.freeze({ url: '/' });

export const TRAVERSE_LIMIT = 3000;
65 changes: 53 additions & 12 deletions src/execute/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { identity } from 'ramda';
import { identity, has } from 'ramda';
import { isPlainObject, isNonEmptyString } from 'ramda-adjunct';
import {
test as testServerURLTemplate,
Expand All @@ -7,7 +7,7 @@ import {
import { ApiDOMStructuredError } from '@swagger-api/apidom-error';
import { url } from '@swagger-api/apidom-reference/configuration/empty';

import { DEFAULT_BASE_URL, DEFAULT_OPENAPI_3_SERVER } from '../constants.js';
import { DEFAULT_BASE_URL, DEFAULT_OPENAPI_3_SERVER, TRAVERSE_LIMIT } from '../constants.js';
import stockHttp from '../http/index.js';
import { serializeRequest } from '../http/serializers/request/index.js';
import SWAGGER2_PARAMETER_BUILDERS from './swagger2/parameter-builders.js';
Expand All @@ -20,6 +20,52 @@ import { serialize as serializeCookie } from '../helpers/cookie.js';

const arrayOrEmpty = (ar) => (Array.isArray(ar) ? ar : []);

const findObjectSchema = (schema, { recurse = true, depth = 1 } = {}) => {
if (!isPlainObject(schema)) return undefined;

// check if the schema is an object type
if (schema.type === 'object' || (Array.isArray(schema.type) && schema.type.includes('object'))) {
return schema;
}

if (depth > TRAVERSE_LIMIT) return undefined;

if (recurse) {
// traverse oneOf keyword first
const oneOfResult = Array.isArray(schema.oneOf)
? schema.oneOf.find((subschema) => findObjectSchema(subschema, { recurse, depth: depth + 1 }))
: undefined;

if (oneOfResult) return oneOfResult;

// traverse anyOf keyword second
const anyOfResult = Array.isArray(schema.anyOf)
? schema.anyOf.find((subschema) => findObjectSchema(subschema, { recurse, depth: depth + 1 }))
: undefined;

if (anyOfResult) return anyOfResult;
}

return undefined;
};

const parseJsonObject = ({ value, silentFail = false }) => {
try {
const parsedValue = JSON.parse(value);
if (typeof parsedValue === 'object') {
return parsedValue;
}
if (!silentFail) {
throw new Error('Expected JSON serialized object');
}
} catch {
if (!silentFail) {
throw new Error('Could not parse object parameter value string as JSON Object');
}
}
return value;
};

/**
* `parseURIReference` function simulates the behavior of `node:url` parse function.
* New WHATWG URL API is not capable of parsing relative references natively,
Expand Down Expand Up @@ -256,16 +302,11 @@ export function buildRequest(options) {
throw new Error(`Required parameter ${parameter.name} is not provided`);
}

if (
specIsOAS3 &&
parameter.schema &&
parameter.schema.type === 'object' &&
typeof value === 'string'
) {
try {
value = JSON.parse(value);
} catch (e) {
throw new Error('Could not parse object parameter value string as JSON');
if (specIsOAS3 && typeof value === 'string') {
if (has('type', parameter.schema) && findObjectSchema(parameter.schema, { recurse: false })) {
value = parseJsonObject({ value, silentFail: false });
} else if (findObjectSchema(parameter.schema, { recurse: true })) {
value = parseJsonObject({ value, silentFail: true });
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/resolver/specmap/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import allOf from './lib/all-of.js';
import parameters from './lib/parameters.js';
import properties from './lib/properties.js';
import ContextTree from './lib/context-tree.js';
import { TRAVERSE_LIMIT } from '../../constants.js';

const PLUGIN_DISPATCH_LIMIT = 100;
const TRAVERSE_LIMIT = 3000;
const noop = () => {};

class SpecMap {
Expand Down
188 changes: 188 additions & 0 deletions test/oas3/execute/style-explode/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -1091,6 +1091,194 @@ describe('OAS 3.0 - buildRequest w/ `style` & `explode` - query parameters', ()
});
});

test('should build a query parameter in form/explode format with oneOf subschema', () => {
// Given
const spec = {
openapi: '3.0.0',
paths: {
'/users': {
get: {
operationId: 'myOperation',
parameters: [
{
in: 'query',
name: 'parameters',
style: 'form',
explode: true,
schema: {
oneOf: [
{
type: 'object',
},
],
},
},
],
},
},
},
};
// when
const req = buildRequest({
spec,
operationId: 'myOperation',
parameters: {
'query.parameters': '{\n "role": "admin",\n "firstname": "alex"\n}',
},
});

expect(req).toEqual({
method: 'GET',
url: `/users?role=admin&firstname=alex`,
credentials: 'same-origin',
headers: {},
});
});

test('should build a query parameter in form/explode format with anyOf subschema', () => {
// Given
const spec = {
openapi: '3.0.0',
paths: {
'/users': {
get: {
operationId: 'myOperation',
parameters: [
{
in: 'query',
name: 'parameters',
style: 'form',
explode: true,
schema: {
anyOf: [
{
type: 'object',
},
],
},
},
],
},
},
},
};
// when
const req = buildRequest({
spec,
operationId: 'myOperation',
parameters: {
'query.parameters': '{\n "role": "admin",\n "firstname": "alex"\n}',
},
});

expect(req).toEqual({
method: 'GET',
url: `/users?role=admin&firstname=alex`,
credentials: 'same-origin',
headers: {},
});
});

test('should build a query parameter in form/explode format with nested subschemas', () => {
// Given
const spec = {
openapi: '3.0.0',
paths: {
'/users': {
get: {
operationId: 'myOperation',
parameters: [
{
in: 'query',
name: 'parameters',
style: 'form',
explode: true,
schema: {
anyOf: [
{
oneOf: [
{
oneOf: [
{
anyOf: [
{
type: ['string', 'object'],
},
],
},
],
},
],
},
],
},
},
],
},
},
},
};
// when
const req = buildRequest({
spec,
operationId: 'myOperation',
parameters: {
'query.parameters': '{\n "role": "admin",\n "firstname": "alex"\n}',
},
});

expect(req).toEqual({
method: 'GET',
url: `/users?role=admin&firstname=alex`,
credentials: 'same-origin',
headers: {},
});
});

test('should build a query parameter in form/explode format when object type is an array element', () => {
// Given
const spec = {
openapi: '3.0.0',
paths: {
'/users': {
get: {
operationId: 'myOperation',
parameters: [
{
in: 'query',
name: 'parameters',
style: 'form',
explode: true,
schema: {
anyOf: [
{
type: ['string', 'object'],
},
],
},
},
],
},
},
},
};
// when
const req = buildRequest({
spec,
operationId: 'myOperation',
parameters: {
'query.parameters': '{\n "role": "admin",\n "firstname": "alex"\n}',
},
});

expect(req).toEqual({
method: 'GET',
url: `/users?role=admin&firstname=alex`,
credentials: 'same-origin',
headers: {},
});
});

test('should build a query parameter in form/no-explode format', () => {
// Given
const spec = {
Expand Down

0 comments on commit 1c968ca

Please sign in to comment.