effect-bdd 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/Bdd.d.ts +0 -1
- package/dist/Bdd.js +1 -2
- package/dist/Errors.d.ts +0 -1
- package/dist/Errors.js +1 -2
- package/dist/bin.d.ts +0 -1
- package/dist/bin.js +1 -2
- package/dist/index.d.ts +0 -1
- package/dist/index.js +1 -2
- package/dist/internal/cli/errors.d.ts +0 -1
- package/dist/internal/cli/errors.js +1 -2
- package/dist/internal/cli/glob.d.ts +0 -1
- package/dist/internal/cli/glob.js +1 -2
- package/dist/internal/cli/loaders.d.ts +0 -1
- package/dist/internal/cli/loaders.js +1 -2
- package/dist/internal/cli/models.d.ts +0 -1
- package/dist/internal/cli/models.js +1 -2
- package/dist/internal/cli/moduleLoader.d.ts +0 -1
- package/dist/internal/cli/moduleLoader.js +1 -2
- package/dist/internal/cli/reporter.d.ts +0 -1
- package/dist/internal/cli/reporter.js +1 -2
- package/dist/internal/cli/runner.d.ts +0 -1
- package/dist/internal/cli/runner.js +1 -2
- package/dist/internal/cli/tagExpression.d.ts +0 -1
- package/dist/internal/cli/tagExpression.js +1 -2
- package/dist/internal/cucumberCompiler.d.ts +0 -1
- package/dist/internal/cucumberCompiler.js +1 -2
- package/dist/internal/expression.d.ts +0 -1
- package/dist/internal/expression.js +1 -2
- package/dist/internal/matching.d.ts +0 -1
- package/dist/internal/matching.js +1 -2
- package/dist/internal/parser.d.ts +0 -1
- package/dist/internal/parser.js +1 -2
- package/dist/internal/runner.d.ts +0 -1
- package/dist/internal/runner.js +1 -2
- package/dist/main.d.ts +0 -1
- package/dist/main.js +1 -2
- package/package.json +4 -10
- package/dist/Bdd.d.ts.map +0 -1
- package/dist/Bdd.js.map +0 -1
- package/dist/Errors.d.ts.map +0 -1
- package/dist/Errors.js.map +0 -1
- package/dist/bin.d.ts.map +0 -1
- package/dist/bin.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/internal/cli/errors.d.ts.map +0 -1
- package/dist/internal/cli/errors.js.map +0 -1
- package/dist/internal/cli/glob.d.ts.map +0 -1
- package/dist/internal/cli/glob.js.map +0 -1
- package/dist/internal/cli/loaders.d.ts.map +0 -1
- package/dist/internal/cli/loaders.js.map +0 -1
- package/dist/internal/cli/models.d.ts.map +0 -1
- package/dist/internal/cli/models.js.map +0 -1
- package/dist/internal/cli/moduleLoader.d.ts.map +0 -1
- package/dist/internal/cli/moduleLoader.js.map +0 -1
- package/dist/internal/cli/reporter.d.ts.map +0 -1
- package/dist/internal/cli/reporter.js.map +0 -1
- package/dist/internal/cli/runner.d.ts.map +0 -1
- package/dist/internal/cli/runner.js.map +0 -1
- package/dist/internal/cli/tagExpression.d.ts.map +0 -1
- package/dist/internal/cli/tagExpression.js.map +0 -1
- package/dist/internal/cucumberCompiler.d.ts.map +0 -1
- package/dist/internal/cucumberCompiler.js.map +0 -1
- package/dist/internal/expression.d.ts.map +0 -1
- package/dist/internal/expression.js.map +0 -1
- package/dist/internal/matching.d.ts.map +0 -1
- package/dist/internal/matching.js.map +0 -1
- package/dist/internal/parser.d.ts.map +0 -1
- package/dist/internal/parser.js.map +0 -1
- package/dist/internal/runner.d.ts.map +0 -1
- package/dist/internal/runner.js.map +0 -1
- package/dist/main.d.ts.map +0 -1
- package/dist/main.js.map +0 -1
- package/src/Bdd.ts +0 -575
- package/src/Errors.ts +0 -60
- package/src/bin.ts +0 -10
- package/src/index.ts +0 -155
- package/src/internal/cli/errors.ts +0 -20
- package/src/internal/cli/glob.ts +0 -37
- package/src/internal/cli/loaders.ts +0 -100
- package/src/internal/cli/models.ts +0 -118
- package/src/internal/cli/moduleLoader.ts +0 -41
- package/src/internal/cli/reporter.ts +0 -367
- package/src/internal/cli/runner.ts +0 -336
- package/src/internal/cli/tagExpression.ts +0 -173
- package/src/internal/cucumberCompiler.ts +0 -58
- package/src/internal/expression.ts +0 -103
- package/src/internal/matching.ts +0 -81
- package/src/internal/parser.ts +0 -155
- package/src/internal/runner.ts +0 -373
- package/src/main.ts +0 -169
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
import * as Arr from "effect/Array"
|
|
2
|
-
import * as Effect from "effect/Effect"
|
|
3
|
-
import { pipe } from "effect/Function"
|
|
4
|
-
import * as Str from "effect/String"
|
|
5
|
-
import { DiscoveryError } from "./errors.ts"
|
|
6
|
-
|
|
7
|
-
/** @internal */
|
|
8
|
-
export type TagPredicate = (tags: ReadonlyArray<string>) => boolean
|
|
9
|
-
|
|
10
|
-
type Expression =
|
|
11
|
-
| {
|
|
12
|
-
readonly _tag: "Tag"
|
|
13
|
-
readonly tag: string
|
|
14
|
-
}
|
|
15
|
-
| {
|
|
16
|
-
readonly _tag: "Not"
|
|
17
|
-
readonly expression: Expression
|
|
18
|
-
}
|
|
19
|
-
| {
|
|
20
|
-
readonly _tag: "And"
|
|
21
|
-
readonly left: Expression
|
|
22
|
-
readonly right: Expression
|
|
23
|
-
}
|
|
24
|
-
| {
|
|
25
|
-
readonly _tag: "Or"
|
|
26
|
-
readonly left: Expression
|
|
27
|
-
readonly right: Expression
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
interface ParseResult {
|
|
31
|
-
readonly expression: Expression
|
|
32
|
-
readonly index: number
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** @internal */
|
|
36
|
-
export const compileAll = (
|
|
37
|
-
expressions: ReadonlyArray<string>
|
|
38
|
-
): Effect.Effect<TagPredicate, DiscoveryError> =>
|
|
39
|
-
Effect.forEach(expressions, compile).pipe(
|
|
40
|
-
Effect.map((predicates) => (tags) => Arr.every(predicates, (predicate) => predicate(tags)))
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
const compile = (expression: string): Effect.Effect<TagPredicate, DiscoveryError> => {
|
|
44
|
-
const tokens = tokenize(expression)
|
|
45
|
-
if (tokens === undefined || tokens.length === 0) {
|
|
46
|
-
return fail(expression, "Expected a tag expression")
|
|
47
|
-
}
|
|
48
|
-
const result = parseOr(tokens, 0)
|
|
49
|
-
if (result === undefined || result.index !== tokens.length) {
|
|
50
|
-
return fail(expression, "Could not parse tag expression")
|
|
51
|
-
}
|
|
52
|
-
return Effect.succeed((tags) => evaluate(result.expression, tags))
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const parseOr = (tokens: ReadonlyArray<string>, index: number): ParseResult | undefined => {
|
|
56
|
-
const left = parseAnd(tokens, index)
|
|
57
|
-
if (left === undefined) {
|
|
58
|
-
return undefined
|
|
59
|
-
}
|
|
60
|
-
return parseOrRest(tokens, left)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const parseOrRest = (tokens: ReadonlyArray<string>, left: ParseResult): ParseResult => {
|
|
64
|
-
if (tokens[left.index] !== "or") {
|
|
65
|
-
return left
|
|
66
|
-
}
|
|
67
|
-
const right = parseAnd(tokens, left.index + 1)
|
|
68
|
-
if (right === undefined) {
|
|
69
|
-
return left
|
|
70
|
-
}
|
|
71
|
-
return parseOrRest(tokens, {
|
|
72
|
-
expression: {
|
|
73
|
-
_tag: "Or",
|
|
74
|
-
left: left.expression,
|
|
75
|
-
right: right.expression
|
|
76
|
-
},
|
|
77
|
-
index: right.index
|
|
78
|
-
})
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const parseAnd = (tokens: ReadonlyArray<string>, index: number): ParseResult | undefined => {
|
|
82
|
-
const left = parseUnary(tokens, index)
|
|
83
|
-
if (left === undefined) {
|
|
84
|
-
return undefined
|
|
85
|
-
}
|
|
86
|
-
return parseAndRest(tokens, left)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const parseAndRest = (tokens: ReadonlyArray<string>, left: ParseResult): ParseResult => {
|
|
90
|
-
if (tokens[left.index] !== "and") {
|
|
91
|
-
return left
|
|
92
|
-
}
|
|
93
|
-
const right = parseUnary(tokens, left.index + 1)
|
|
94
|
-
if (right === undefined) {
|
|
95
|
-
return left
|
|
96
|
-
}
|
|
97
|
-
return parseAndRest(tokens, {
|
|
98
|
-
expression: {
|
|
99
|
-
_tag: "And",
|
|
100
|
-
left: left.expression,
|
|
101
|
-
right: right.expression
|
|
102
|
-
},
|
|
103
|
-
index: right.index
|
|
104
|
-
})
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const parseUnary = (tokens: ReadonlyArray<string>, index: number): ParseResult | undefined =>
|
|
108
|
-
tokens[index] === "not" ? parseNot(tokens, index) : parsePrimary(tokens, index)
|
|
109
|
-
|
|
110
|
-
const parseNot = (tokens: ReadonlyArray<string>, index: number): ParseResult | undefined => {
|
|
111
|
-
const result = parseUnary(tokens, index + 1)
|
|
112
|
-
return result === undefined
|
|
113
|
-
? undefined
|
|
114
|
-
: {
|
|
115
|
-
expression: {
|
|
116
|
-
_tag: "Not",
|
|
117
|
-
expression: result.expression
|
|
118
|
-
},
|
|
119
|
-
index: result.index
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const parsePrimary = (tokens: ReadonlyArray<string>, index: number): ParseResult | undefined => {
|
|
124
|
-
const token = tokens[index]
|
|
125
|
-
if (token === undefined) {
|
|
126
|
-
return undefined
|
|
127
|
-
}
|
|
128
|
-
if (token === "(") {
|
|
129
|
-
const result = parseOr(tokens, index + 1)
|
|
130
|
-
return result !== undefined && tokens[result.index] === ")"
|
|
131
|
-
? {
|
|
132
|
-
expression: result.expression,
|
|
133
|
-
index: result.index + 1
|
|
134
|
-
}
|
|
135
|
-
: undefined
|
|
136
|
-
}
|
|
137
|
-
return pipe(token, Str.startsWith("@"))
|
|
138
|
-
? {
|
|
139
|
-
expression: {
|
|
140
|
-
_tag: "Tag",
|
|
141
|
-
tag: token
|
|
142
|
-
},
|
|
143
|
-
index: index + 1
|
|
144
|
-
}
|
|
145
|
-
: undefined
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const evaluate = (expression: Expression, tags: ReadonlyArray<string>): boolean => {
|
|
149
|
-
switch (expression._tag) {
|
|
150
|
-
case "Tag": {
|
|
151
|
-
return Arr.contains(expression.tag)(tags)
|
|
152
|
-
}
|
|
153
|
-
case "Not": {
|
|
154
|
-
return !evaluate(expression.expression, tags)
|
|
155
|
-
}
|
|
156
|
-
case "And": {
|
|
157
|
-
return evaluate(expression.left, tags) && evaluate(expression.right, tags)
|
|
158
|
-
}
|
|
159
|
-
case "Or": {
|
|
160
|
-
return evaluate(expression.left, tags) || evaluate(expression.right, tags)
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const tokenize = (expression: string): ReadonlyArray<string> | undefined => {
|
|
166
|
-
const matches = expression.match(/\(|\)|\b(?:and|or|not)\b|@[A-Za-z0-9][A-Za-z0-9_-]*/g) ?? []
|
|
167
|
-
const normalized = pipe(expression, Str.replace(/\s+/g, ""))
|
|
168
|
-
const matched = pipe(matches, Arr.join(""))
|
|
169
|
-
return normalized === matched ? matches : undefined
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const fail = (expression: string, message: string): Effect.Effect<never, DiscoveryError> =>
|
|
173
|
-
Effect.fail(new DiscoveryError({ message: `${message}: ${expression}` }))
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { AstBuilder, compile, GherkinClassicTokenMatcher, Parser } from "@cucumber/gherkin"
|
|
2
|
-
import { IdGenerator } from "@cucumber/messages"
|
|
3
|
-
import * as Effect from "effect/Effect"
|
|
4
|
-
import * as Layer from "effect/Layer"
|
|
5
|
-
import { ParseError } from "../Errors.ts"
|
|
6
|
-
import { GherkinCompiler, type ParsedSource } from "./parser.ts"
|
|
7
|
-
|
|
8
|
-
/** @internal */
|
|
9
|
-
export const Cucumber = Layer.succeed(GherkinCompiler, {
|
|
10
|
-
compile: (source, uri) =>
|
|
11
|
-
Effect.try({
|
|
12
|
-
try: () => compileWithCucumber(source, uri),
|
|
13
|
-
catch: parseErrorFromCause
|
|
14
|
-
})
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
const compileWithCucumber = (source: string, uri: string): ParsedSource => {
|
|
18
|
-
const newId = IdGenerator.incrementing()
|
|
19
|
-
const parser = new Parser(new AstBuilder(newId), new GherkinClassicTokenMatcher())
|
|
20
|
-
const document = parser.parse(source)
|
|
21
|
-
return {
|
|
22
|
-
document,
|
|
23
|
-
pickles: compile(document, uri, newId)
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const parseErrorFromCause = (cause: unknown): ParseError => {
|
|
28
|
-
const location = causeLocation(cause)
|
|
29
|
-
return new ParseError({
|
|
30
|
-
message: causeMessage(cause),
|
|
31
|
-
line: location?.line ?? 1,
|
|
32
|
-
column: location?.column ?? 1
|
|
33
|
-
})
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const causeLocation = (cause: unknown): { readonly line: number; readonly column?: number } | undefined => {
|
|
37
|
-
if (
|
|
38
|
-
typeof cause === "object" && cause !== null && "errors" in cause && Array.isArray(cause.errors) &&
|
|
39
|
-
cause.errors.length > 0
|
|
40
|
-
) {
|
|
41
|
-
return causeLocation(cause.errors[0])
|
|
42
|
-
}
|
|
43
|
-
if (typeof cause === "object" && cause !== null && "location" in cause) {
|
|
44
|
-
const location = cause.location
|
|
45
|
-
if (typeof location === "object" && location !== null && "line" in location && typeof location.line === "number") {
|
|
46
|
-
return {
|
|
47
|
-
line: location.line,
|
|
48
|
-
...("column" in location && typeof location.column === "number" ? { column: location.column } : {})
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return undefined
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const causeMessage = (cause: unknown): string =>
|
|
56
|
-
typeof cause === "object" && cause !== null && "message" in cause && typeof cause.message === "string"
|
|
57
|
-
? cause.message
|
|
58
|
-
: String(cause)
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import * as Arr from "effect/Array"
|
|
2
|
-
import { pipe } from "effect/Function"
|
|
3
|
-
import * as Option from "effect/Option"
|
|
4
|
-
import * as Record from "effect/Record"
|
|
5
|
-
import * as Schema from "effect/Schema"
|
|
6
|
-
import * as Str from "effect/String"
|
|
7
|
-
|
|
8
|
-
/** @internal */
|
|
9
|
-
export interface Capture<Name extends string, A> {
|
|
10
|
-
readonly _tag: "Capture"
|
|
11
|
-
readonly name: Name
|
|
12
|
-
readonly schema: Schema.Codec<A, string>
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** @internal */
|
|
16
|
-
export interface Matcher<A> {
|
|
17
|
-
readonly source: string
|
|
18
|
-
readonly match: (text: string) => Option.Option<A>
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
interface MatcherState {
|
|
22
|
-
readonly names: ReadonlyArray<string>
|
|
23
|
-
readonly captures: ReadonlyArray<Capture<string, unknown>>
|
|
24
|
-
readonly source: string
|
|
25
|
-
readonly pattern: string
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** @internal */
|
|
29
|
-
export const makeCapture = <const Name extends string, A>(
|
|
30
|
-
name: Name,
|
|
31
|
-
schema: Schema.Codec<A, string>
|
|
32
|
-
): Capture<Name, A> => ({
|
|
33
|
-
_tag: "Capture",
|
|
34
|
-
name,
|
|
35
|
-
schema
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
/** @internal */
|
|
39
|
-
export const makeMatcher = <A>(
|
|
40
|
-
strings: TemplateStringsArray,
|
|
41
|
-
captures: ReadonlyArray<Capture<string, unknown>>
|
|
42
|
-
): Matcher<A> => {
|
|
43
|
-
const state = pipe(
|
|
44
|
-
strings,
|
|
45
|
-
Arr.reduce(initialMatcherState, (state, literal, index) => appendTemplatePart(state, literal, captures[index]))
|
|
46
|
-
)
|
|
47
|
-
const regex = new globalThis.RegExp(`${state.pattern}$`)
|
|
48
|
-
const decoders = Arr.map(state.captures, (capture) => Schema.decodeUnknownOption(capture.schema))
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
source: state.source,
|
|
52
|
-
match(text) {
|
|
53
|
-
const match = regex.exec(text)
|
|
54
|
-
if (match === null) {
|
|
55
|
-
return Option.none()
|
|
56
|
-
}
|
|
57
|
-
return Option.map(decodeCaptures(state.names, decoders, match), (out) => out as A)
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const initialMatcherState: MatcherState = {
|
|
63
|
-
names: [],
|
|
64
|
-
captures: [],
|
|
65
|
-
source: "",
|
|
66
|
-
pattern: "^"
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const appendTemplatePart = (
|
|
70
|
-
state: MatcherState,
|
|
71
|
-
literal: string,
|
|
72
|
-
capture: Capture<string, unknown> | undefined
|
|
73
|
-
): MatcherState => {
|
|
74
|
-
const pattern = `${state.pattern}${escapeRegExp(literal)}`
|
|
75
|
-
const source = `${state.source}${literal}`
|
|
76
|
-
if (capture === undefined) {
|
|
77
|
-
return { ...state, pattern, source }
|
|
78
|
-
}
|
|
79
|
-
return {
|
|
80
|
-
names: Arr.append(state.names, capture.name),
|
|
81
|
-
captures: Arr.append(state.captures, capture),
|
|
82
|
-
pattern: `${pattern}(.+?)`,
|
|
83
|
-
source: `${source}{${capture.name}}`
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const decodeCaptures = (
|
|
88
|
-
names: ReadonlyArray<string>,
|
|
89
|
-
decoders: ReadonlyArray<(input: unknown) => Option.Option<unknown>>,
|
|
90
|
-
match: RegExpExecArray,
|
|
91
|
-
index = 0,
|
|
92
|
-
out: Record<string, unknown> = Record.empty()
|
|
93
|
-
): Option.Option<Record<string, unknown>> => {
|
|
94
|
-
if (index >= names.length) {
|
|
95
|
-
return Option.some(out)
|
|
96
|
-
}
|
|
97
|
-
return pipe(
|
|
98
|
-
decoders[index](match[index + 1]),
|
|
99
|
-
Option.flatMap((value) => decodeCaptures(names, decoders, match, index + 1, Record.set(out, names[index], value)))
|
|
100
|
-
)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const escapeRegExp = Str.replace(/[/\\^$*+?.()|[\]{}]/g, "\\$&")
|
package/src/internal/matching.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { type PickleStep, PickleStepType } from "@cucumber/messages"
|
|
2
|
-
import * as Arr from "effect/Array"
|
|
3
|
-
import { pipe } from "effect/Function"
|
|
4
|
-
import * as Option from "effect/Option"
|
|
5
|
-
|
|
6
|
-
/** @internal */
|
|
7
|
-
export type StepKind = "Step" | "Given" | "When" | "Then"
|
|
8
|
-
|
|
9
|
-
/** @internal */
|
|
10
|
-
export type ConcreteStepKind = "Given" | "When" | "Then"
|
|
11
|
-
|
|
12
|
-
/** @internal */
|
|
13
|
-
export interface MatchableTransition {
|
|
14
|
-
readonly kind: StepKind
|
|
15
|
-
readonly expression: {
|
|
16
|
-
readonly source: string
|
|
17
|
-
readonly match: (text: string) => Option.Option<unknown>
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** @internal */
|
|
22
|
-
export interface MatchedTransition<Transition extends MatchableTransition> {
|
|
23
|
-
readonly transition: Transition
|
|
24
|
-
readonly captures: unknown
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** @internal */
|
|
28
|
-
export const matchingTextTransitions = <Transition extends MatchableTransition>(
|
|
29
|
-
transitions: ReadonlyArray<Transition>,
|
|
30
|
-
text: string
|
|
31
|
-
): ReadonlyArray<MatchedTransition<Transition>> =>
|
|
32
|
-
pipe(
|
|
33
|
-
transitions,
|
|
34
|
-
Arr.map((transition): Option.Option<MatchedTransition<Transition>> =>
|
|
35
|
-
pipe(
|
|
36
|
-
transition.expression.match(text),
|
|
37
|
-
Option.map((captures) => ({ transition, captures }))
|
|
38
|
-
)
|
|
39
|
-
),
|
|
40
|
-
Arr.getSomes
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
/** @internal */
|
|
44
|
-
export const matchingKeywordTransitions = <Transition extends MatchableTransition>(
|
|
45
|
-
transitions: ReadonlyArray<MatchedTransition<Transition>>,
|
|
46
|
-
kind: ConcreteStepKind
|
|
47
|
-
): ReadonlyArray<MatchedTransition<Transition>> =>
|
|
48
|
-
Arr.filter(transitions, (match) => keywordMatches(match.transition.kind, kind))
|
|
49
|
-
|
|
50
|
-
/** @internal */
|
|
51
|
-
export const keywordMatches = (transition: StepKind, keyword: ConcreteStepKind): boolean =>
|
|
52
|
-
transition === "Step" || transition === keyword
|
|
53
|
-
|
|
54
|
-
/** @internal */
|
|
55
|
-
export const concreteStepKind = (step: PickleStep): Option.Option<ConcreteStepKind> => {
|
|
56
|
-
switch (step.type) {
|
|
57
|
-
case PickleStepType.CONTEXT: {
|
|
58
|
-
return Option.some("Given")
|
|
59
|
-
}
|
|
60
|
-
case PickleStepType.ACTION: {
|
|
61
|
-
return Option.some("When")
|
|
62
|
-
}
|
|
63
|
-
case PickleStepType.OUTCOME: {
|
|
64
|
-
return Option.some("Then")
|
|
65
|
-
}
|
|
66
|
-
default: {
|
|
67
|
-
return Option.none()
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** @internal */
|
|
73
|
-
export const renderTransitionKinds = <Transition extends MatchableTransition>(
|
|
74
|
-
transitions: ReadonlyArray<Transition>
|
|
75
|
-
): string =>
|
|
76
|
-
pipe(
|
|
77
|
-
transitions,
|
|
78
|
-
Arr.map((transition) => transition.kind),
|
|
79
|
-
Arr.dedupe,
|
|
80
|
-
Arr.join(", ")
|
|
81
|
-
)
|
package/src/internal/parser.ts
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
Background as CucumberBackground,
|
|
3
|
-
GherkinDocument,
|
|
4
|
-
Pickle,
|
|
5
|
-
Rule as CucumberRule,
|
|
6
|
-
Scenario as CucumberScenario,
|
|
7
|
-
Step as CucumberStep
|
|
8
|
-
} from "@cucumber/messages"
|
|
9
|
-
import * as Context from "effect/Context"
|
|
10
|
-
import * as Effect from "effect/Effect"
|
|
11
|
-
import { pipe } from "effect/Function"
|
|
12
|
-
import * as Option from "effect/Option"
|
|
13
|
-
import { ParseError } from "../Errors.ts"
|
|
14
|
-
|
|
15
|
-
/** @internal */
|
|
16
|
-
export type Keyword = "Given" | "When" | "Then" | "And" | "But" | "*"
|
|
17
|
-
|
|
18
|
-
/** @internal */
|
|
19
|
-
export interface CompiledFeature {
|
|
20
|
-
readonly name: string
|
|
21
|
-
readonly line: number
|
|
22
|
-
readonly pickles: ReadonlyArray<Pickle>
|
|
23
|
-
readonly source: SourceIndex
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** @internal */
|
|
27
|
-
export interface ParsedSource {
|
|
28
|
-
readonly document: GherkinDocument
|
|
29
|
-
readonly pickles: ReadonlyArray<Pickle>
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** @internal */
|
|
33
|
-
export class GherkinCompiler extends Context.Service<GherkinCompiler, {
|
|
34
|
-
readonly compile: (source: string, uri: string) => Effect.Effect<ParsedSource, ParseError>
|
|
35
|
-
}>()("effect-bdd/GherkinCompiler") {}
|
|
36
|
-
|
|
37
|
-
/** @internal */
|
|
38
|
-
export interface SourceIndex {
|
|
39
|
-
readonly steps: ReadonlyMap<string, CucumberStep>
|
|
40
|
-
readonly scenarios: ReadonlyMap<string, {
|
|
41
|
-
readonly scenario: CucumberScenario
|
|
42
|
-
readonly rule: CucumberRule | undefined
|
|
43
|
-
}>
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** @internal */
|
|
47
|
-
export const parse = (source: string, uri = "<inline>"): Effect.Effect<CompiledFeature, ParseError, GherkinCompiler> =>
|
|
48
|
-
Effect.flatMap(GherkinCompiler, (compiler) => pipe(compiler.compile(source, uri), Effect.flatMap(toFeature)))
|
|
49
|
-
|
|
50
|
-
const toFeature: (parsed: ParsedSource) => Effect.Effect<CompiledFeature, ParseError> = Effect.fnUntraced(function*(
|
|
51
|
-
parsed
|
|
52
|
-
) {
|
|
53
|
-
const feature = parsed.document.feature
|
|
54
|
-
if (feature === undefined) {
|
|
55
|
-
return yield* parseError("Expected a Feature declaration", 1, 1)
|
|
56
|
-
}
|
|
57
|
-
if (parsed.pickles.length === 0) {
|
|
58
|
-
return yield* parseError("Expected at least one Scenario", feature.location.line, feature.location.column ?? 1)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const source = indexDocument(parsed.document)
|
|
62
|
-
return {
|
|
63
|
-
name: feature.name,
|
|
64
|
-
line: feature.location.line,
|
|
65
|
-
pickles: parsed.pickles,
|
|
66
|
-
source
|
|
67
|
-
}
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
const indexDocument = (document: GherkinDocument): SourceIndex => {
|
|
71
|
-
const steps = new Map<string, CucumberStep>()
|
|
72
|
-
const scenarios = new Map<string, { readonly scenario: CucumberScenario; readonly rule: CucumberRule | undefined }>()
|
|
73
|
-
const feature = document.feature
|
|
74
|
-
if (feature === undefined) {
|
|
75
|
-
return { steps, scenarios }
|
|
76
|
-
}
|
|
77
|
-
for (const child of feature.children) {
|
|
78
|
-
if (child.background !== undefined) {
|
|
79
|
-
indexBackground(steps, child.background)
|
|
80
|
-
}
|
|
81
|
-
if (child.scenario !== undefined) {
|
|
82
|
-
indexScenario(steps, scenarios, child.scenario, undefined)
|
|
83
|
-
}
|
|
84
|
-
if (child.rule !== undefined) {
|
|
85
|
-
for (const ruleChild of child.rule.children) {
|
|
86
|
-
if (ruleChild.background !== undefined) {
|
|
87
|
-
indexBackground(steps, ruleChild.background)
|
|
88
|
-
}
|
|
89
|
-
if (ruleChild.scenario !== undefined) {
|
|
90
|
-
indexScenario(steps, scenarios, ruleChild.scenario, child.rule)
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return { steps, scenarios }
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const indexBackground = (steps: Map<string, CucumberStep>, background: CucumberBackground): void => {
|
|
99
|
-
for (const step of background.steps) {
|
|
100
|
-
steps.set(step.id, step)
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const indexScenario = (
|
|
105
|
-
steps: Map<string, CucumberStep>,
|
|
106
|
-
scenarios: Map<string, { readonly scenario: CucumberScenario; readonly rule: CucumberRule | undefined }>,
|
|
107
|
-
scenario: CucumberScenario,
|
|
108
|
-
rule: CucumberRule | undefined
|
|
109
|
-
): void => {
|
|
110
|
-
scenarios.set(scenario.id, { scenario, rule })
|
|
111
|
-
for (const step of scenario.steps) {
|
|
112
|
-
steps.set(step.id, step)
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** @internal */
|
|
117
|
-
export const findScenario = (pickle: Pickle, index: SourceIndex) =>
|
|
118
|
-
pipe(
|
|
119
|
-
pickle.astNodeIds,
|
|
120
|
-
Option.liftPredicate((ids): ids is ReadonlyArray<string> => ids.length > 0),
|
|
121
|
-
Option.flatMap(() => Option.fromNullishOr(pickle.astNodeIds.find((id) => index.scenarios.has(id)))),
|
|
122
|
-
Option.flatMap((id) => Option.fromNullishOr(index.scenarios.get(id)))
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
/** @internal */
|
|
126
|
-
export const findStep = (
|
|
127
|
-
pickleStep: { readonly astNodeIds: ReadonlyArray<string> },
|
|
128
|
-
index: SourceIndex
|
|
129
|
-
): CucumberStep | undefined =>
|
|
130
|
-
pickleStep.astNodeIds
|
|
131
|
-
.map((id) => index.steps.get(id))
|
|
132
|
-
.find((step) => step !== undefined)
|
|
133
|
-
|
|
134
|
-
/** @internal */
|
|
135
|
-
export const stepLine = (
|
|
136
|
-
step: { readonly astNodeIds: ReadonlyArray<string> },
|
|
137
|
-
index: SourceIndex
|
|
138
|
-
): number => findStep(step, index)?.location.line ?? 1
|
|
139
|
-
|
|
140
|
-
/** @internal */
|
|
141
|
-
export const stepKeyword = (
|
|
142
|
-
step: { readonly astNodeIds: ReadonlyArray<string> },
|
|
143
|
-
index: SourceIndex
|
|
144
|
-
): Keyword => {
|
|
145
|
-
const source = findStep(step, index)
|
|
146
|
-
return source === undefined ? "Given" : normalizeKeyword(source.keyword)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const normalizeKeyword = (keyword: string): Keyword => {
|
|
150
|
-
const trimmed = keyword.trim()
|
|
151
|
-
return trimmed === "*" ? "*" : trimmed as Keyword
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const parseError = (message: string, line: number, column: number): Effect.Effect<never, ParseError> =>
|
|
155
|
-
Effect.fail(new ParseError({ message, line, column }))
|