github unkn0wn-root/resterm v0.52.1

4 hours ago

v0.52.1

This release ships many bug fixes and improvements across the parser, mocks, and RestermScript, along with two larger additions: mock request matching is far more expressive, and RestermScript now has a switch statement with labeled break and continue.

Breaking change: default() removed in favour of ??

default(a, b), rts.default(a, b), and stdlib.default(a, b) no longer exist, and default is a reserved word. Replace every call with ??:

default(vars.get("token"), "anon")     # removed
(vars.get("token") ?? "anon")          # replacement

Keep the parentheses when the call was part of a larger expression. ?? binds looser than everything except the ternary, so default(a, b) + c becomes (a ?? b) + c, not a ?? b + c.

The replacement is also lazy, which the old call was not. A fallback now runs only when it is actually needed:

default(a, fail("missing"))   # always failed
a ?? fail("missing")          # fails only when a is null

Check any fallback that calls uuid(), mutates vars, or fails. Because default is reserved, let default = 1, fn default() {}, {default: 1}, and value.default are now parse errors, with a message that points at the fix:

let cfg = {"default": 1}
let v = cfg["default"]

The same rule reaches directives that create a binding: # @use ./mod.rts as switch and # @for-each items as case are rejected instead of creating a name nothing can reference. Module names cannot be reserved words either.

RestermScript: switch

switch picks one branch out of many. The tagged form compares a value, the tagless form replaces a long if/elif chain.

switch response.statusCode {
case 200, 201:
  result = "success"
case 401:
  result = "unauthorized"
default:
  result = "unexpected"
}

switch {
case score >= 90:
  grade = "A"
default:
  grade = "C"
}

The tag is evaluated once, clauses run top to bottom, and only the matching clause runs. There is no fallthrough. Each clause is its own scope, and at most one default is allowed anywhere among the cases. This is separate from the @switch workflow directive, which still routes workflow steps.

RestermScript: labeled break and continue

A plain break inside a switch only leaves the switch, so a label is what lets you leave the surrounding loop:

outer: for let i, code range codes {
  switch code {
  case 401, 403:
    found = code
    break outer
  }
}

continue label resumes the named loop and skips the rest of everything in between:

outer: for let i, row range rows {
  for let j, value range row {
    switch value {
    case null:
      continue outer
    }
  }
}

Labels only decorate for and switch, they live in their own namespace so they never collide with a variable, and continue must name a loop.

Mocks: richer matching

Headers and query now share one rule set

contains, regex, and oneOf join exact, prefix, present, and absent, and all of them now work on query parameters too:

# @match headers={"User-Agent":{"contains":"Chrome"},"X-Api-Version":{"regex":"^v[0-9]+$"},"X-Env":{"oneOf":["dev","stage","prod"]}}
# @match query={"amount":{"gte":1000},"channel":{"oneOf":["web","ios"]},"dry-run":{"absent":true}}

gt, gte, lt, and lte are query only. Regex matching is unanchored, so use ^ and $ for a whole value. oneOf ignores order and extra values, unlike an exact array. A value that does not rnary non-match rather than an error, so ?amount=none simply fails {"gte":1000}.

Header names stay case-insensitive, query names are case-sensitive, and values are case-sensitive in both.

Match the request body with json-rules

json still matches a literal body subset. The new json-rules compares values, and the two combine:

# @match json={"kind":"personal"}
# @match json-rules={"score":{"lt":40},"tier":{"oneOf":["free","trial"]},"limits":{"daily":{"gte":1,"lt":100}}}

Everything inside json is treated as request data, so {"$gt":100} or a field literally named gt needs no escaping. In json-rules, an operator name is still usable as a field name by nesting another rule inside it:

# matches {"range": {"gt": 125}}
# @match json-rules={"range":{"gt":{"gt":100}}}

Bad rules are reported with their location instead of silently never matching:

invalid json-rules matcher at amount.gtt: expected a rule object or a known operator

To match a body that is itself a JSON string, quote the JSON inside the option value. json=100 matches the number, json='"100"' matches the string.

Long matchers can be split

If a bracket is still open, the matcher continues on the next comment line:

# @match json-rules={
#   "score": {"lt": 40},
#   "tier":  {"oneOf": ["free", "trial"]}
# }

Unrelated fields can also be split across separate declarations, which are merged. Repeating the same field is an error. When Resterm rewrites the file it writes the merged matcher on one line.

mock.count and mock.received

The script helpers accept every new operator plus jsonRules, using the same schema as @match:

mock.count({
  method: "POST",
  path: "/orders",
  query: {page: {gte: 10}},
  headers: {"X-Env": {oneOf: ["dev", "prod"]}},
  jsonRules: {amount: {gt: 100}}
})

Directives are validated instead of quietly ignored

A whole class of silent mistakes now gets reported.

Repeated options are errors

# @mock method=GET path=/a path=/b
# -> @mock option "path" is repeated

and alternate form of the same option conflict:

# @ssh host=h known-hosts=a known_hosts=b
# -> @ssh options "known-hosts", "known_hosts" are the same option

Empty values are ignored for regular options but not for switches, since strict_hostkey= already enables the switch. @compare is stricter still, so base= or baseline= with nothing after it is reported rather than treated as omitted.

Missing and duplicate values

Missing values are reported for directives that require one, including @name, @operation, @grpc-descriptor, @grpc-authority, and @grpc-metadata:

# @name
# -> @name value missing

Duplicate request directives that replace a single value are caught, and the first valid declaration wins:

# @name Alpha
# @name Beta
# -> @name directive already defined for this request

This covers @auth, @name, @timeout, @when, @for-each, @trace, @profile, @compare, and the single value gRPC and GraphQL directives. Additive directives such as @tag, @capture, @ar are unaffected. An invalid declaration does not count, so a valid one may follow it.

Saving a file with parse errors

Files with parse errors can still be saved, and the status line says so:

Saved requests.http (1 parse error)

Fixes

  • @graphql off now resets the request properly. The operation, variables, and query collected so far are discarded, so a request can reconfigure them after re-enabling GraphQL.
# @graphql
# @operation First
POST {{graphql.endpoint}}
# @graphql off
# @graphql
# @operation Second
  • @grpc with a malformed method used to be ignored and the request would run against nothing. It is reported now:
# @grpc EchoService
# -> invalid @grpc method "EchoService", use package.Service/Method
  • @grpc-metadata lines without a colon or with an empty key were dropped in silence and are now reported (use key: value). @grpc-reflection and @grpc-plaintext accept every boolean form anstead of treating any unrecognised value as true.
  • Expectations() on a mock handler returned patterns that still aliased the live handler's rules.
  • Mock capture writes query conditions as explicit exact rules, so a captured mock round trips through the new matcher model unchanged.
  • Editor completion offers json-rules=, and the writer preserves it when rewriting a file.

Don't miss a new resterm release

NewReleases is sending notifications on new releases.