Skip to content
This repository has been archived by the owner on Aug 28, 2024. It is now read-only.

chore: new package api #14

Merged
merged 27 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e1b9130
refactor: add new API (wip)
hanspagel Feb 14, 2024
53bea68
refactor: move new api to source folder
hanspagel Feb 14, 2024
a3a5f56
chore: test references
hanspagel Feb 15, 2024
cdf264f
refactor: rename API methods
hanspagel Feb 15, 2024
2aa0e72
docs: update README
hanspagel Feb 15, 2024
7f414bf
feat: validate + resolve
hanspagel Feb 15, 2024
55ae2a6
feat: add upgrade function (wip)
hanspagel Feb 15, 2024
3767670
refactor: move files around, tweak api (wip)
hanspagel Feb 16, 2024
ec7df53
refactor: properly integrate upgrade API
hanspagel Feb 16, 2024
6ac0243
refactor: improve error messages
hanspagel Feb 16, 2024
899df2f
feat: work on file references (wip)
hanspagel Feb 16, 2024
94cedf7
refactor: add filesystem support to resolveRefs (wip)
hanspagel Feb 16, 2024
ed5baf6
feat: add filter method
hanspagel Feb 16, 2024
2d0fe23
refactor: add details method
hanspagel Feb 16, 2024
01cd28b
feat: make the parser work with multiple files
hanspagel Feb 16, 2024
0c2047d
chore: add a ton of tests
hanspagel Feb 19, 2024
7d25643
test: mark failing tests as todo
hanspagel Feb 19, 2024
b942eb2
refactor: move Validator to separate class
hanspagel Feb 19, 2024
b0e9f84
refactor: split Validator methods in separate files
hanspagel Feb 19, 2024
56332f1
fix: ref encoding
hanspagel Feb 19, 2024
f91cb3f
chore: better error handling for non-existing files
hanspagel Feb 19, 2024
101bb3b
chore: try to fix file references with pointers (wip)
hanspagel Feb 19, 2024
eeac7dc
chore: improve types
hanspagel Feb 22, 2024
dbbd1d8
chore: clean up, improve types, add some more tests
hanspagel Feb 22, 2024
1060fef
chore: prepare release
hanspagel Feb 23, 2024
5cc275f
docs(changeset): refactor: rewrite the whole package :)
hanspagel Feb 23, 2024
a363578
ci: disable real world tests for now
hanspagel Feb 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
5 changes: 5 additions & 0 deletions .changeset/dry-drinks-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@scalar/openapi-parser": patch
---

refactor: rewrite the whole package :)
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ npm add @scalar/openapi-parser

## Usage

### Parse
### Validate

```ts
import { parse } from '@scalar/openapi-parser'
import { validate } from '@scalar/openapi-parser'

const file = `{
"openapi": "3.1.0",
Expand All @@ -40,13 +40,19 @@ const file = `{
"paths": {}
}`

const result = await parse(file)
const result = await validate(file)

console.log(result.valid)

if (!result.valid) {
console.log(result.errors)
}
```

### Validate
### Resolve references

```ts
import { validate } from '@scalar/openapi-parser'
import { resolve } from '@scalar/openapi-parser'

const file = `{
"openapi": "3.1.0",
Expand All @@ -57,13 +63,7 @@ const file = `{
"paths": {}
}`

const result = await validate(file)

console.log(result.valid)

if (!result.valid) {
console.log(result.errors)
}
const result = await resolve(file)
```

## Community
Expand Down
8 changes: 4 additions & 4 deletions packages/openapi-parser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ npm add @scalar/openapi-parser
### Parse

```ts
import { parse } from '@scalar/openapi-parser'
import { resolve } from '@scalar/openapi-parser'

const file = `{
"openapi": "3.1.0",
Expand All @@ -30,7 +30,7 @@ const file = `{
"paths": {}
}`

const result = await parse(file)
const result = await resolve(file)
```

### Validate
Expand Down Expand Up @@ -59,7 +59,7 @@ if (!result.valid) {
### Version

```ts
import { parse } from '@scalar/openapi-parser'
import { resolve } from '@scalar/openapi-parser'

const file = `{
"openapi": "3.1.0",
Expand All @@ -70,7 +70,7 @@ const file = `{
"paths": {}
}`

const result = await parse(file)
const result = await resolve(file)

console.log(result.version)
```
7 changes: 4 additions & 3 deletions packages/openapi-parser/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,11 @@
</div>

<script type="module">
import { parse } from './src/index.ts'
import { openapi } from './src/index.ts'

// Initial parse
const input = document.getElementById('input').value
const output = await parse(input)
const output = await openapi().load(input).upgrade().resolve()
document.getElementById('output').innerHTML = JSON.stringify(
output,
null,
Expand All @@ -108,7 +108,8 @@
document.getElementById('input').addEventListener('input', async (e) => {
const input = e.target.value
try {
const output = await parse(input)
const output = await openapi().load(input).upgrade().resolve()

document.getElementById('output').innerHTML = JSON.stringify(
output,
null,
Expand Down
6 changes: 5 additions & 1 deletion packages/openapi-parser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,15 @@
"test": "vitest"
},
"devDependencies": {
"@modyfi/vite-plugin-yaml": "^1.1.0",
"@types/js-yaml": "^4.0.9",
"vite-plugin-dts": "^3.7.3"
},
"dependencies": {
"@humanwhocodes/momoa": "^3.0.1",
"@types/node": "^20.11.19",
"@hyperjump/browser": "^1.1.3",
"@hyperjump/json-schema": "^1.7.2",
"ajv": "^8.12.0",
"ajv-draft-04": "^1.0.0",
"ajv-formats": "^2.1.1",
Expand All @@ -51,6 +54,7 @@
"jsonpointer": "^5.0.1",
"leven": "^4.0.0",
"openapi-types": "^12.1.3",
"vite": "^5.1.4"
"vite": "^5.1.4",
"yaml": "^2.3.4"
}
}
31 changes: 31 additions & 0 deletions packages/openapi-parser/src/configuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Ajv04 from 'ajv-draft-04'
import Ajv2020 from 'ajv/dist/2020'

/**
* A list of the supported OpenAPI versions
*/
export const supportedVersions = ['2.0', '3.0', '3.1'] as const

export type SupportedVersion = (typeof supportedVersions)[number]

/**
* Configure available JSON Schema versions
*/
export const jsonSchemaVersions = {
'http://json-schema.org/draft-04/schema#': Ajv04,
'https://json-schema.org/draft/2020-12/schema': Ajv2020,
}

/**
* List of error messages used in the Validator
*/
export const ERRORS = {
EMPTY_OR_INVALID: 'Cannot find JSON, YAML or filename in data',
// URI_MUST_BE_STRING: 'uri parameter or $id attribute must be a string',
OPENAPI_VERSION_NOT_SUPPORTED:
'Cannot find supported Swagger/OpenAPI version in specification, version must be a string.',
} as const

export type VALIDATOR_ERROR = keyof typeof ERRORS

export const inlinedRefs = 'x-inlined-refs'
1 change: 1 addition & 0 deletions packages/openapi-parser/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './lib'
export * from './types'
export * from './utils'
export * from './pipeline'
9 changes: 9 additions & 0 deletions packages/openapi-parser/src/lib/Validator/Validator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { describe, expect, it } from 'vitest'

import { Validator } from './Validator'

describe('Validator', () => {
it('returns all supported versions', () => {
expect(Validator.supportedVersions).toMatchObject(['2.0', '3.0', '3.1'])
})
})
151 changes: 151 additions & 0 deletions packages/openapi-parser/src/lib/Validator/Validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import addFormats from 'ajv-formats'

import {
ERRORS,
type SupportedVersion,
inlinedRefs,
jsonSchemaVersions,
supportedVersions,
} from '../../configuration'
import type { AnyObject, Filesystem, ValidateResult } from '../../types'
import { details as getOpenApiVersion } from '../../utils'
import { checkReferences } from './checkReferences'
import { resolveReferences } from './resolveReferences'
import { transformErrors } from './transformErrors'

export class Validator {
public version: string

public static supportedVersions = supportedVersions

// Object with function *or* object { errors: string }
protected ajvValidators: Record<
string,
((specification: AnyObject) => boolean) & {
errors: string
}
> = {}

protected externalRefs: Record<string, AnyObject> = {}

protected errors: string

protected specificationVersion: string

protected specificationType: string

public specification: AnyObject

resolveReferences(filesystem?: Filesystem) {
return resolveReferences(filesystem, true)
}

/**
* Checks whether a specification is valid and all references can be resolved.
*/
async validate(filesystem: Filesystem): Promise<ValidateResult> {
const entrypoint = filesystem.find((file) => file.entrypoint)
const specification = entrypoint?.specification

// TODO: How does this work with a filesystem?
this.specification = specification

try {
// AnyObject is empty or invalid
if (specification === undefined || specification === null) {
return {
valid: false,
errors: transformErrors(entrypoint, ERRORS.EMPTY_OR_INVALID),
}
}

// TODO: Do we want to keep external references in the spec?
if (Object.keys(this.externalRefs).length > 0) {
specification[inlinedRefs] = this.externalRefs
}

// Meta data about the specification
const { version, specificationType, specificationVersion } =
getOpenApiVersion(specification)

this.version = version
this.specificationVersion = specificationVersion
this.specificationType = specificationType

// AnyObject is not supported
if (!version) {
return {
valid: false,
errors: transformErrors(
entrypoint,
ERRORS.OPENAPI_VERSION_NOT_SUPPORTED,
),
}
}

// Get the correct OpenAPI validator
const validateSchema = await this.getAjvValidator(version)
const schemaResult = validateSchema(specification)

// Check if the references are valid
if (schemaResult) {
return checkReferences(filesystem)
}

// Error handling
if (validateSchema.errors) {
if (validateSchema.errors.length > 0) {
return {
valid: false,
errors: transformErrors(entrypoint, validateSchema.errors),
}
}
}

// Whoops … no errors? Actually, that should never happen.
return {
valid: false,
}
} catch (error) {
// Something went horribly wrong!
return {
valid: false,
errors: transformErrors(entrypoint, error.message ?? error),
}
}
}

/**
* Ajv JSON schema validator
*/
async getAjvValidator(version: SupportedVersion) {
// Schema loaded already
if (this.ajvValidators[version]) {
return this.ajvValidators[version]
}

// Load OpenAPI Schema
const schema = await import(`../../../schemas/v${version}/schema.json`)

// Load JSON Schema
const AjvClass = jsonSchemaVersions[schema.$schema]

// Get the correct Ajv validator
const ajv = new AjvClass({
// Ajv is a bit too strict in its strict validation of OpenAPI schemas.
// Switch strict mode off.
strict: false,
})

// Register formats
// https://ajv.js.org/packages/ajv-formats.html#formats
addFormats(ajv)

// OpenAPI 3.1 uses media-range format
if (version === '3.1') {
ajv.addFormat('media-range', true)
}

return (this.ajvValidators[version] = ajv.compile(schema))
}
}
Loading