Skip to content
This repository has been archived by the owner on Jun 27, 2022. It is now read-only.

Adhers to the specification #46

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,9 @@ jobs:
name: Build
command: yarn build
- run:
name: Tests
name: Unit tests
command: yarn test

- run:
name: Spefication tests
command: yarn spec:all
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"cucumberautocomplete.steps": ["specification/support/steps.ts"],
"cucumberautocomplete.strictGherkinCompletion": true
}
13 changes: 13 additions & 0 deletions cucumber.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const options = [
'--require-module ts-node/register',
'--require specification/support/*.ts',
'--publish-quiet',
]

module.exports = {
default: [
'node_modules/@universal-path/specification/features/**/*.feature',
...options,
].join(' '),
only: options.join(' '),
}
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module.exports = {
transform: {
'^.+\\.tsx?$': 'ts-jest',
'^.+\\.ts$': 'ts-jest',
},
}
14 changes: 13 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,23 @@
"scripts": {
"test": "jest",
"clean": "rimraf -rf lib",
"spec": "cucumber-js -p only",
"spec:all": "cucumber-js -p default",
"spec:watch": "ts-node specification/scripts/run.ts",
"build": "yarn clean && rollup -c rollup.config.js",
"prepublishOnly": "yarn test && yarn build"
},
"devDependencies": {
"@babel/core": "^7.13.10",
"@babel/preset-env": "^7.13.12",
"@cucumber/cucumber": "^7.3.1",
"@types/chai": "^4.2.21",
"@types/debug": "^4.1.7",
"@types/jest": "^26.0.21",
"@universal-path/specification": "^0.2.1",
"chai": "^4.3.4",
"chokidar": "^3.5.2",
"debug": "^4.3.2",
"jest": "^26.6.3",
"rimraf": "^3.0.2",
"rollup": "^2.42.3",
Expand All @@ -43,6 +53,8 @@
"rollup-plugin-sourcemaps": "^0.6.3",
"rollup-plugin-typescript2": "^0.30.0",
"ts-jest": "^26.5.4",
"ts-node": "^10.2.1",
"typescript": "^4.2.3"
}
},
"dependencies": {}
}
23 changes: 23 additions & 0 deletions specification/scripts/run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { watch } from 'chokidar'
import { spawnSync } from 'child_process'

const runTests = (filePath: string) => {
console.clear()
spawnSync(
'cucumber-js',
[
filePath,
'--require-module=ts-node/register',
'--require=specification/support/*.ts',
'--publish-quiet',
],
{
stdio: 'inherit',
},
)
}

watch('specification/**/*.feature')
.on('add', runTests)
.on('change', runTests)
.on('unlink', runTests)
39 changes: 39 additions & 0 deletions specification/support/steps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { expect } from 'chai'
import { Given, When, Then } from '@cucumber/cucumber'

Given('the path is {string}', function (path: string) {
this.path = path
})

When('the url is {string}', function (url: string) {
this.url = url
this.match(this.path, this.url)
})

Then('it matches', function () {
expect(this.result).to.have.property('matches', true)
})

Then(`it doesn't match`, function () {
expect(this.result).to.have.property('matches', false)
expect(this.result).to.have.deep.property('params', {})
})

Then('has no parameters', function () {
expect(this.result.params).to.deep.equal({})
})

Then(
'has the {string} parameter equal to {string}',
function (parameterName: string, value: string) {
expect(this.result.params).not.to.be.null

const normalizedValue = value.includes(',')
? value.split(',').map((s) => s.trim())
: value
expect(this.result.params).to.have.deep.property(
parameterName,
normalizedValue,
)
},
)
14 changes: 14 additions & 0 deletions specification/support/world.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { setWorldConstructor } from '@cucumber/cucumber'
import { Path, Match, match } from '../../src'

class CustomWorld {
public path: Path
public url: string
public result: Match

match() {
this.result = match(this.path, this.url)
}
}

setWorldConstructor(CustomWorld)
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { Path, Match, match } from './match'
export { pathToRegExp } from './pathToRegExp'
export * from './match'
export * from './toRegExp'
33 changes: 29 additions & 4 deletions src/match.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,52 @@
import { pathToRegExp } from './pathToRegExp'
import { parse } from './parse'
import { toRegExp } from './toRegExp'

const log = require('debug')('match')

export type Path = RegExp | string
export type Params = Record<string, string | string[]>

export interface Match {
matches: boolean
params: Record<string, string> | null
params: Record<string, string>
}

/**
* Matches a given url against a path.
*/
export const match = (path: Path, url: string): Match => {
const expression = path instanceof RegExp ? path : pathToRegExp(path)
log('matching "%s" adainst "%s"...', path, url)

const expression = path instanceof RegExp ? path : toRegExp(parse(path))
log('using expression', expression)

const match = expression.exec(url) || false
log('match result', match)

// Matches in strict mode: match string should equal to input (url)
// Otherwise loose matches will be considered truthy:
// match('/messages/:id', '/messages/123/users') // true
const matches =
path instanceof RegExp ? !!match : !!match && match[0] === match.input

let params = {}

if (matches && match) {
params = Object.entries(match.groups || {}).reduce<Params>(
(params, [name, value]) => {
if (typeof value !== 'undefined') {
params[name] = value.includes('/') ? value.split('/') : value
}
return params
},
{},
)
}

log('parameters', params)

return {
matches,
params: match && matches ? match.groups || null : null,
params,
}
}
5 changes: 5 additions & 0 deletions src/parse.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { parse } from './parse'

it('parses', () => {
parse('https://api.site.com/user/:id/:role?')
})
45 changes: 45 additions & 0 deletions src/parse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
const SEGMENT_EXP = /(?<![:\/])\//g
const PARAM_EXP = /^:([\w_-]+)(\?|\*|\+)?$/

export interface TokenType<Type extends string> {
type: Type
delimiter?: '/'
}

export interface StringToken extends TokenType<'string'> {
value: string
}

export interface ParameterToken extends TokenType<'parameter'> {
name: string
modifier: string
}

export type Token = StringToken | ParameterToken

export function parse(path: string): Token[] {
const segments = path.split(SEGMENT_EXP)
const tokens = segments.map<Token>((segment, index) => {
const isLastToken = index === segments.length - 1
const delimiter = isLastToken ? undefined : '/'

const paramMatch = segment.match(PARAM_EXP)
if (paramMatch) {
const [, name, modifier] = paramMatch
return {
type: 'parameter',
name,
modifier,
delimiter,
}
}

return {
type: 'string',
value: segment,
delimiter,
}
})

return tokens
}
76 changes: 0 additions & 76 deletions src/pathToRegExp.test.ts

This file was deleted.

26 changes: 0 additions & 26 deletions src/pathToRegExp.ts

This file was deleted.

Loading