Skip to content

Commit

Permalink
add mergeCombinarySibling option
Browse files Browse the repository at this point in the history
  • Loading branch information
udamir committed Aug 26, 2023
1 parent 60bd307 commit c420378
Show file tree
Hide file tree
Showing 7 changed files with 272 additions and 171 deletions.
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<img alt="npm" src="https://img.shields.io/npm/v/allof-merge"> <img alt="npm" src="https://img.shields.io/npm/dm/allof-merge?label=npm"> ![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/udamir/allof-merge/ci.yml)
<img alt="npm type definitions" src="https://img.shields.io/npm/types/allof-merge"> ![Coveralls branch](https://img.shields.io/coverallsCoverage/github/udamir/allof-merge) <img alt="GitHub" src="https://img.shields.io/github/license/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
Expand All @@ -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

Expand Down Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
27 changes: 22 additions & 5 deletions src/merge.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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))

Expand Down
17 changes: 14 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ export type JsonSchema = any
export type MergeRules = CrawlRules<MergeRule>

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
}

Expand All @@ -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 {
Expand Down
10 changes: 9 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { JsonPath, isObject } from "json-crawl"

import { RefNode } from "./types"
import { AnyOfNode, OneOfNode, RefNode } from "./types"

export class MapArray<K, V> extends Map<K, Array<V>> {
public add(key: K, value: V): this {
Expand Down Expand Up @@ -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("#")

Expand Down
158 changes: 0 additions & 158 deletions test/merge.jsonschema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down
Loading

0 comments on commit c420378

Please sign in to comment.