routing.ts 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. import { Duration } from "effect"
  2. import { OpenApiMethods, type OpenApiSpec, type Options, type Result, type Scenario } from "./types"
  3. type ScenarioTimeout = `${number} ${Duration.Unit}`
  4. const durationUnits = new Set<string>([
  5. "nano",
  6. "nanos",
  7. "micro",
  8. "micros",
  9. "milli",
  10. "millis",
  11. "second",
  12. "seconds",
  13. "minute",
  14. "minutes",
  15. "hour",
  16. "hours",
  17. "day",
  18. "days",
  19. "week",
  20. "weeks",
  21. ])
  22. export function routeKeys(spec: OpenApiSpec) {
  23. return Object.entries(spec.paths ?? {})
  24. .flatMap(([path, item]) =>
  25. OpenApiMethods.filter((method) => item[method]).map((method) => `${method.toUpperCase()} ${path}`),
  26. )
  27. .sort()
  28. }
  29. export function routeKey(scenario: Scenario) {
  30. return `${scenario.method} ${scenario.path}`
  31. }
  32. export function coverageResult(scenario: Scenario): Result {
  33. if (scenario.kind === "todo") return { status: "skip", scenario }
  34. return { status: "pass", scenario }
  35. }
  36. export function parseOptions(args: string[]): Options {
  37. const mode = option(args, "--mode") ?? "effect"
  38. if (mode !== "effect" && mode !== "coverage" && mode !== "auth") throw new Error(`invalid --mode ${mode}`)
  39. return {
  40. mode,
  41. include: option(args, "--include"),
  42. startAt: option(args, "--start-at"),
  43. stopAt: option(args, "--stop-at"),
  44. failOnMissing: args.includes("--fail-on-missing"),
  45. failOnSkip: args.includes("--fail-on-skip"),
  46. scenarioTimeout: parseScenarioTimeout(option(args, "--scenario-timeout") ?? "30 seconds"),
  47. progress: args.includes("--progress"),
  48. trace: args.includes("--trace"),
  49. }
  50. }
  51. export function matches(options: Options, scenario: Scenario) {
  52. if (!options.include) return true
  53. return (
  54. scenario.name.includes(options.include) ||
  55. scenario.path.includes(options.include) ||
  56. scenario.method.includes(options.include.toUpperCase())
  57. )
  58. }
  59. export function selectedScenarios(options: Options, scenarios: Scenario[]) {
  60. const included = scenarios.filter((scenario) => matches(options, scenario))
  61. const start = options.startAt ? included.findIndex((scenario) => matchesName(options.startAt!, scenario)) : 0
  62. const end = options.stopAt
  63. ? included.findIndex((scenario) => matchesName(options.stopAt!, scenario))
  64. : included.length - 1
  65. if (start === -1) throw new Error(`--start-at matched no scenario: ${options.startAt}`)
  66. if (end === -1) throw new Error(`--stop-at matched no scenario: ${options.stopAt}`)
  67. return included.slice(start, end + 1)
  68. }
  69. function matchesName(value: string, scenario: Scenario) {
  70. return scenario.name.includes(value) || scenario.path.includes(value) || scenario.method.includes(value.toUpperCase())
  71. }
  72. function option(args: string[], name: string) {
  73. const index = args.indexOf(name)
  74. if (index === -1) return undefined
  75. return args[index + 1]
  76. }
  77. function parseScenarioTimeout(input: string) {
  78. if (!isScenarioTimeout(input)) throw new Error(`invalid --scenario-timeout ${input}`)
  79. return Duration.fromInputUnsafe(input)
  80. }
  81. function isScenarioTimeout(input: string): input is ScenarioTimeout {
  82. const [amount, unit, extra] = input.trim().split(/\s+/)
  83. return extra === undefined && amount !== undefined && Number.isFinite(Number(amount)) && durationUnits.has(unit ?? "")
  84. }