Skip to content

Compare two Json based API documents (OpenAPI, AsyncAPI, JsonSchema, GraphAPI)

License

Notifications You must be signed in to change notification settings

udamir/api-smart-diff

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

api-smart-diff

npm npm npm type definitions GitHub

This package provides utils to compute the diff between two Json based API documents - online demo

Purpose

  • Generate API changelog
  • Identify breaking changes
  • Ensure API versioning consistency

Supported API specifications

Features

  • Generate diff for supported specifications
  • Generate merged document with changes in metadata
  • Classify all changes as breaking, non-breaking, deprecated and annotation
  • Human-readable change description
  • Supports custom classification rules
  • Supports custom comparison or match rules
  • Supports custom transformations
  • Supports custom human-readable changes annotation
  • Resolves all $ref pointers, including circular
  • Typescript syntax support out of the box
  • Can be used in nodejs or browser

External $ref

If schema contains an external $ref, you should bundle it via api-ref-bundler first.

Installation

npm install api-smart-diff --save

or

yarn add api-smart-diff

Usage

Nodejs

import { apiCompare } from 'api-smart-diff'

const { diffs, merged } = apiCompare(before, after)
// diff: 
// {
//   action: "add" | "remove" | "replace" | "rename",
//   after: 'value in after',
//   before: 'value in before',
//   description: 'human-readable description'
//   path: ['path, 'in', 'array', 'format'],
//   type: "annotation" | "breaking" | "non-breaking" | "unclassified" | "deprecated"
// } 

// merged meta:
// {
//   action: "add" | "remove" | "replace" | "rename",
//   type: "annotation" | "breaking" | "non-breaking" | "unclassified" | "deprecated",
//   replaced: "value in before",
// }

Browsers

A browser version of api-smart-diff is also available via CDN:

<script src="https://cdn.jsdelivr.net/npm/api-smart-diff@latest/dist/api-smart-diff.min.js"></script>

Reference api-smart-diff.min.js in your HTML and use the global variable ApiSmartDiff.

<script>
  var { diffs, merged } = ApiSmartDiff.apiCompare(before, after)
</script>

Documentation

Package provides the following public functions:

apiCompare (before, after, options?: CompareOptions): { diffs: Diff[], merged: object }

Calculates the difference and merge two objects and classify difference in accordinance with before document type

apiCompare(before, after, options)

The apiDiff function calculates the difference between two objects.

Arguments

  • before: any - the origin object
  • after: any - the object being compared structurally with the origin object\
  • options: CompareOptions [optional] - comparison options
export type ComapreOptions = {
  rules?: CompareRules              // custom rules for compare

  metaKey?: string | symbol         // metakey for merge changes
  arrayMeta?: boolean               // add changes to arrays via metakey
  annotateHook?: AnnotateHook       // custom format hook

  externalSources?: {               // resolved external $ref sources
    before?: Record<string, object>
    after?: Record<string, object>
  }
}

Result

Function returns object with diffs array and merged object with metadata

type Diff = {
  action: "add" | "remove" | "replace" | "rename"
  path: Array<string | number>
  description?: string 
  before?: any
  after?: any
  type: "breaking" | "non-breaking" | "annotation" | "unclassified" | "deprecated"
}

type MergeMeta = DiffMeta | MergeArrayMeta
type MergeArrayMeta = { array: Record<number, MergeMeta> }

export type DiffMeta = {
  action: "add" | "remove" | "replace" | "rename"
  type: "breaking" | "non-breaking" | "annotation" | "unclassified" | "deprecated"
  replaced?: any
}

Example

const metaKey = Symbol("diff")
const { diffs, merged } = apiCompare(before, after, { metaKey })

// do something with diffs or merged object

Custom rules

Custom compare rules can be defined as CrawlRules:

import { CrawlRules } from "json-crawl"

type CompareRules = CrawlRules<CompareRule>

type CompareRule = {
  $?: ClassifyRule                            // classifier for current node
  compare?: CompareResolver                   // compare handler for current node
  transform?: CompareTransformResolver[]      // transformations before compare/merge
  mapping?: MappingResolver<string | number>  // keys mapping rules
  annotate?: ChangeAnnotationResolver         // resolver for annotation template
}

// Change classifier
type ClassifyRule = [ 
  DiffType | (ctx: ComapreContext) => DiffType, // add
  DiffType | (ctx: ComapreContext) => DiffType, // remove
  DiffType | (ctx: ComapreContext) => DiffType, // replace (rename)
  DiffType | (ctx: ComapreContext) => DiffType, // (optional) reversed rule for add
  DiffType | (ctx: ComapreContext) => DiffType, // (optional) reversed rule for remove
  DiffType | (ctx: ComapreContext) => DiffType  // (optional) reversed rule for replace (rename)
]

// Compare context
type ComapreContext = {
  before: NodeContext       // before node context
  after: NodeContext        // after node context
  rules: CompareRules       // rules for compared nodes
  options: ComapreOptions   // compare options
}

// Node context
type NodeContext =  {
  path: JsonPath
  key: string | number
  value: unknown
  parent?: unknown
  root: unknown
}

// Custom compare resolver
type CompareResolver = (ctx: ComapreContext) => CompareResult | void

// Transformation rules
type CompareTransformResolver<T = unknown> = (before: T, after: T) => [T, T]

// Mapping rules
type MappingResolver = (
  before: Record<string, unknown> | unknown[],
  after: Record<string, unknown> | unknown[], 
  ctx: ComapreContext
) => MapKeysResult

type MapKeysResult<T extends string | number> = {
  added: Array<T>
  removed: Array<T>
  mapped: Record<T, T>
}

// Annotation tempalte resolver
type ChangeAnnotationResolver = (diff: Diff, ctx: ComapreContext) => AnnotateTemplate | undefined

type AnnotateTemplate = {
  template: string,
  params?: { [key: string]: AnnotateTemplate | string | number | undefined }
}

Contributing

When contributing, keep in mind that it is an objective of api-smart-diff to have no additional package dependencies. This may change in the future, but for now, no new dependencies.

Please run the unit tests before submitting your PR: yarn test. Hopefully your PR includes additional unit tests to illustrate your change/modification!

License

MIT

About

Compare two Json based API documents (OpenAPI, AsyncAPI, JsonSchema, GraphAPI)

Resources

License

Stars

Watchers

Forks

Packages

No packages published