diff --git a/src/constants.js b/src/constants.js index 4dab69da2..356208613 100644 --- a/src/constants.js +++ b/src/constants.js @@ -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; diff --git a/src/execute/index.js b/src/execute/index.js index 2c1a02676..0d58f1621 100755 --- a/src/execute/index.js +++ b/src/execute/index.js @@ -1,4 +1,4 @@ -import { identity } from 'ramda'; +import { identity, has } from 'ramda'; import { isPlainObject, isNonEmptyString } from 'ramda-adjunct'; import { test as testServerURLTemplate, @@ -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'; @@ -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, @@ -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 }); } } diff --git a/src/resolver/specmap/index.js b/src/resolver/specmap/index.js index 383360147..aae9a184c 100644 --- a/src/resolver/specmap/index.js +++ b/src/resolver/specmap/index.js @@ -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 { diff --git a/test/oas3/execute/style-explode/query.js b/test/oas3/execute/style-explode/query.js index 135166e0f..f47b0d13e 100644 --- a/test/oas3/execute/style-explode/query.js +++ b/test/oas3/execute/style-explode/query.js @@ -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 = {