diff --git a/README.md b/README.md
index ae51534..1131cfc 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/udamir/allof-merge/ci.yml)
![Coveralls branch](https://img.shields.io/coverallsCoverage/github/udamir/allof-merge)
-Merge schemas combined using allOf into a more readable composed schema free from allOf.
+Merge schemas using allOf into a more readable composed schema free from allOf.
## Features
- Safe merging of schemas combined with allOf in whole JsonSchema based document
@@ -13,7 +13,8 @@ Merge schemas combined using allOf into a more readable composed schema free fro
- Correctly merge items and additionalItems taking into account common validations
- Supports rules extension to merge other document types and JsonSchema versions
- Supports $refs and circular references either (internal references only)
-- Correctly merge of sibling content with $refs (optionally)
+- Correctly merge of $refs with sibling content (optionally)
+- Correctly merge of combinaries (anyOf, oneOf) with sibling content (optionally)
- Typescript syntax support out of the box
- No dependencies (except json-crawl), can be used in nodejs or browser
@@ -107,10 +108,14 @@ interface MergeOptions {
// (optional) default = jsonSchemaMergeRules("draft-06")
rules?: MergeRules
- // merge $ref and sibling content
+ // merge $ref with sibling content
// (optional) default = false
mergeRefSibling?: boolean
+ // merge anyOf/oneOf with sibling content
+ // (optional) default = false
+ mergeCombinarySibling?: boolean
+
// Merge error hook, called on any merge conflicts
onMergeError?: (message: string, path: JsonPath, values: any[]) => void
}
diff --git a/package.json b/package.json
index f49e328..0c408a0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "allof-merge",
- "version": "0.3.0",
+ "version": "0.4.0",
"description": "Simplify your JsonSchema by combining allOf safely.",
"module": "dist/esm/index.js",
"main": "dist/cjs/index.js",
diff --git a/src/merge.ts b/src/merge.ts
index a17d007..47d0f57 100644
--- a/src/merge.ts
+++ b/src/merge.ts
@@ -1,6 +1,6 @@
import { JsonPath, SyncCloneHook, isObject, syncClone } from "json-crawl"
-import { buildPointer, isRefNode, parseRef, removeDuplicates, resolvePointer } from "./utils"
+import { buildPointer, isAnyOfNode, isOneOfNode, isRefNode, parseRef, removeDuplicates, resolvePointer } from "./utils"
import { MergeError, MergeOptions, MergeRules } from "./types"
import { jsonSchemaMergeResolver } from "./resolvers"
import { jsonSchemaMergeRules } from "./rules"
@@ -82,12 +82,29 @@ export const allOfResolverHook = (options?: MergeOptions): SyncCloneHook<{}> =>
const _allOf = [...allOf]
- if (!_allOf.length && !(options?.mergeRefSibling && isRefNode(value))) {
- return { value, exitHook }
+ if (!_allOf.length) {
+ if (options?.mergeRefSibling && isRefNode(value)) {
+ // create allOf from $ref and sibling if mergeRefSibling option
+ _allOf.push(sibling)
+ } else if (options?.mergeCombinarySibling && isAnyOfNode(value)) {
+ // include sibling to anyOf if mergeCombinarySibling option
+ const { anyOf, ...rest } = value
+ const _value = Object.keys(rest).length ? { anyOf: anyOf.map((item) => ({ allOf: [item, rest] })) } : value
+ return { value: _value, exitHook }
+ } else if (options?.mergeCombinarySibling && isOneOfNode(value)) {
+ // include sibling to oneOf if mergeCombinarySibling option
+ const { oneOf, ...rest } = value
+ const _value = Object.keys(rest).length ? { oneOf: oneOf.map((item) => ({ allOf: [item, rest] })) } : value
+ return { value: _value, exitHook }
+ }
+ } else {
+ // include sibling to allOf
+ _allOf.push(sibling)
}
- // include sibling to allOf if mergeRefSibling option
- _allOf.push(sibling)
+ if (!_allOf.length) {
+ return { value, exitHook }
+ }
const allOfItems = normalizeAllOfItems(_allOf, source, allOfRefs, buildPointer(ctx.path))
diff --git a/src/types.ts b/src/types.ts
index c2ca6aa..095bea3 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -4,9 +4,10 @@ export type JsonSchema = any
export type MergeRules = CrawlRules
export interface MergeOptions {
- source?: any // source JsonSchema if merging only part of it
- rules?: MergeRules // custom merge rules
- mergeRefSibling?: boolean // merge $ref and sibling content
+ source?: any // source JsonSchema if merging only part of it
+ rules?: MergeRules // custom merge rules
+ mergeRefSibling?: boolean // merge $ref and sibling content
+ mergeCombinarySibling?: boolean // merge oneOf, anyOf sibling content
onMergeError?: (message: string, path: JsonPath, values: any[]) => void
}
@@ -15,6 +16,16 @@ export interface RefNode {
[key: string]: any
}
+export interface AnyOfNode {
+ anyOf: any[]
+ [key: string]: any
+}
+
+export interface OneOfNode {
+ oneOf: any[]
+ [key: string]: any
+}
+
export type MergeError = (values: any[]) => void
export interface MergeContext {
diff --git a/src/utils.ts b/src/utils.ts
index db8fd4d..dae0b09 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -1,6 +1,6 @@
import { JsonPath, isObject } from "json-crawl"
-import { RefNode } from "./types"
+import { AnyOfNode, OneOfNode, RefNode } from "./types"
export class MapArray extends Map> {
public add(key: K, value: V): this {
@@ -44,6 +44,14 @@ export const isRefNode = (value: any): value is RefNode => {
return value && value.$ref && typeof value.$ref === "string"
}
+export const isAnyOfNode = (value: any): value is AnyOfNode => {
+ return value && value.anyOf && Array.isArray(value.anyOf)
+}
+
+export const isOneOfNode = (value: any): value is OneOfNode => {
+ return value && value.oneOf && Array.isArray(value.oneOf)
+}
+
export const parseRef = ($ref: string, basePath = "") => {
const [filePath = basePath, ref] = $ref.split("#")
diff --git a/test/merge.jsonschema.test.ts b/test/merge.jsonschema.test.ts
index e2b73e4..417c2f4 100644
--- a/test/merge.jsonschema.test.ts
+++ b/test/merge.jsonschema.test.ts
@@ -343,164 +343,6 @@ describe("module", function () {
},
})
})
-
- it("should merge $ref sibling with mergeRefSibling option", () => {
- const source = {
- foo: {
- type: "object",
- properties: {
- id: {
- type: "string",
- },
- },
- },
- bar: {
- type: "object",
- properties: {
- permissions: {
- $ref: "#/permission",
- type: "object",
- properties: {
- admin: {
- type: "boolean",
- },
- },
- },
- },
- },
- permission: {
- type: "object",
- properties: {
- level: {
- type: "number",
- },
- },
- },
- }
-
- const result = merge(
- {
- allOf: [
- {
- $ref: "#/foo",
- },
- {
- $ref: "#/bar",
- },
- {
- type: "object",
- properties: {
- name: {
- type: "string",
- },
- },
- },
- ],
- },
- { source, mergeRefSibling: true }
- )
-
- expect(result).toEqual({
- type: "object",
- properties: {
- id: {
- type: "string",
- },
- name: {
- type: "string",
- },
- permissions: {
- type: "object",
- properties: {
- admin: {
- type: "boolean",
- },
- level: {
- type: "number",
- },
- },
- },
- },
- })
- })
-
- it("should not merge $ref sibling without mergeRefSibling option", () => {
- const source = {
- foo: {
- type: "object",
- properties: {
- id: {
- type: "string",
- },
- },
- },
- bar: {
- type: "object",
- properties: {
- permissions: {
- $ref: "#/permission",
- type: "object",
- properties: {
- admin: {
- type: "boolean",
- },
- },
- },
- },
- },
- permission: {
- type: "object",
- properties: {
- level: {
- type: "number",
- },
- },
- },
- }
-
- const result = merge(
- {
- allOf: [
- {
- $ref: "#/foo",
- },
- {
- $ref: "#/bar",
- },
- {
- type: "object",
- properties: {
- name: {
- type: "string",
- },
- },
- },
- ],
- },
- { source }
- )
-
- expect(result).toEqual({
- type: "object",
- properties: {
- id: {
- type: "string",
- },
- name: {
- type: "string",
- },
- permissions: {
- $ref: "#/permission",
- type: "object",
- properties: {
- admin: {
- type: "boolean",
- },
- },
- },
- },
- })
- })
})
describe("simple resolve functionality", function () {
diff --git a/test/merge.sibling.test.ts b/test/merge.sibling.test.ts
new file mode 100644
index 0000000..9c22650
--- /dev/null
+++ b/test/merge.sibling.test.ts
@@ -0,0 +1,218 @@
+import { merge } from "../src"
+
+describe("merge sibling content", function () {
+ it("should merge $ref sibling with mergeRefSibling option", () => {
+ const source = {
+ foo: {
+ type: "object",
+ properties: {
+ id: {
+ type: "string",
+ },
+ },
+ },
+ bar: {
+ type: "object",
+ properties: {
+ permissions: {
+ $ref: "#/permission",
+ type: "object",
+ properties: {
+ admin: {
+ type: "boolean",
+ },
+ },
+ },
+ },
+ },
+ permission: {
+ type: "object",
+ properties: {
+ level: {
+ type: "number",
+ },
+ },
+ },
+ }
+
+ const result = merge(
+ {
+ allOf: [
+ {
+ $ref: "#/foo",
+ },
+ {
+ $ref: "#/bar",
+ },
+ {
+ type: "object",
+ properties: {
+ name: {
+ type: "string",
+ },
+ },
+ },
+ ],
+ },
+ { source, mergeRefSibling: true }
+ )
+
+ expect(result).toEqual({
+ type: "object",
+ properties: {
+ id: {
+ type: "string",
+ },
+ name: {
+ type: "string",
+ },
+ permissions: {
+ type: "object",
+ properties: {
+ admin: {
+ type: "boolean",
+ },
+ level: {
+ type: "number",
+ },
+ },
+ },
+ },
+ })
+ })
+
+ it("should not merge $ref sibling without mergeRefSibling option", () => {
+ const source = {
+ foo: {
+ type: "object",
+ properties: {
+ id: {
+ type: "string",
+ },
+ },
+ },
+ bar: {
+ type: "object",
+ properties: {
+ permissions: {
+ $ref: "#/permission",
+ type: "object",
+ properties: {
+ admin: {
+ type: "boolean",
+ },
+ },
+ },
+ },
+ },
+ permission: {
+ type: "object",
+ properties: {
+ level: {
+ type: "number",
+ },
+ },
+ },
+ }
+
+ const result = merge(
+ {
+ allOf: [
+ {
+ $ref: "#/foo",
+ },
+ {
+ $ref: "#/bar",
+ },
+ {
+ type: "object",
+ properties: {
+ name: {
+ type: "string",
+ },
+ },
+ },
+ ],
+ },
+ { source }
+ )
+
+ expect(result).toEqual({
+ type: "object",
+ properties: {
+ id: {
+ type: "string",
+ },
+ name: {
+ type: "string",
+ },
+ permissions: {
+ $ref: "#/permission",
+ type: "object",
+ properties: {
+ admin: {
+ type: "boolean",
+ },
+ },
+ },
+ },
+ })
+ })
+
+ it("should merges oneOf and sibling with mergeCombinarySibling option", function () {
+ const result = merge({
+ required: ["id"],
+ type: "object",
+ properties: {
+ id: {
+ type: "string",
+ },
+ },
+ oneOf: [
+ {
+ type: "object",
+ properties: {
+ key: {
+ type: "string",
+ },
+ },
+ },
+ {
+ type: "object",
+ additionalProperties: {
+ type: "string",
+ },
+ },
+ ],
+ }, { mergeCombinarySibling: true })
+
+ expect(result).toMatchObject({
+ oneOf: [
+ {
+ required: ["id"],
+ type: "object",
+ properties: {
+ id: {
+ type: "string",
+ },
+ key: {
+ type: "string",
+ },
+ },
+ },
+ {
+ required: ["id"],
+ type: "object",
+ properties: {
+ id: {
+ type: "string",
+ },
+ },
+ additionalProperties: {
+ type: "string",
+ },
+ },
+ ],
+ })
+ })
+})
\ No newline at end of file