///|
/// Location: line and optional column in a text file (Cucumber Messages).
pub struct Location {
line : Int
column : Int?
} derive(ToJson, FromJson)
///|
/// Timestamp: seconds since Unix epoch + nanos (0..999_999_999).
/// `seconds` is an Int64 so that dates after 2038-01-19 fit.
pub struct Timestamp {
seconds : Int64
nanos : Int
}
///|
/// Duration: seconds + nanos (0..999_999_999).
pub struct Duration {
seconds : Int64
nanos : Int
}
///|
/// Decode an integral JSON number into Int64. The core `Int64` JSON
/// codec uses strings, but the protocol encodes `seconds` as a number.
fn seconds_from_json(
json : Json,
path : @json.JsonPath,
) -> Int64 raise @json.JsonDecodeError {
guard json is Number(n, ..) && n == n.floor() else {
raise @json.JsonDecodeError((path, "seconds: expected an integer number"))
}
n.to_int64()
}
///|
fn seconds_and_nanos_from_json(
json : Json,
path : @json.JsonPath,
type_name : String,
) -> (Int64, Int) raise @json.JsonDecodeError {
guard json is Object(obj) else {
raise @json.JsonDecodeError(
(path, "\{type_name}::from_json: expected object"),
)
}
guard obj.get("seconds") is Some(s) else {
raise @json.JsonDecodeError(
(path, "\{type_name}::from_json: missing seconds"),
)
}
guard obj.get("nanos") is Some(n) else {
raise @json.JsonDecodeError(
(path, "\{type_name}::from_json: missing nanos"),
)
}
(
seconds_from_json(s, path.add_key("seconds")),
@json.from_json(n, path=path.add_key("nanos")),
)
}
///|
pub impl @json.FromJson for Timestamp with fn from_json(json, path) {
let (seconds, nanos) = seconds_and_nanos_from_json(json, path, "Timestamp")
Timestamp::{ seconds, nanos, }
}
///|
pub impl ToJson for Timestamp with fn to_json(self) {
{ "seconds": Json::number(self.seconds.to_double()), "nanos": self.nanos }
}
///|
pub impl @json.FromJson for Duration with fn from_json(json, path) {
let (seconds, nanos) = seconds_and_nanos_from_json(json, path, "Duration")
Duration::{ seconds, nanos, }
}
///|
pub impl ToJson for Duration with fn to_json(self) {
{ "seconds": Json::number(self.seconds.to_double()), "nanos": self.nanos }
}
///|
/// Source media type for Source.mediaType.
pub enum SourceMediaType {
GherkinPlain
GherkinMarkdown
}
///|
pub impl @json.FromJson for SourceMediaType with fn from_json(json, path) {
guard json is String(s) else {
raise @json.JsonDecodeError(
(path, "SourceMediaType::from_json: expected string"),
)
}
match s {
"text/x.cucumber.gherkin+plain" => GherkinPlain
"text/x.cucumber.gherkin+markdown" => GherkinMarkdown
_ =>
raise @json.JsonDecodeError(
(
path, "SourceMediaType::from_json: expected text/x.cucumber.gherkin+plain or text/x.cucumber.gherkin+markdown",
),
)
}
}
///|
pub impl ToJson for SourceMediaType with fn to_json(self) {
match self {
GherkinPlain => "text/x.cucumber.gherkin+plain".to_json()
GherkinMarkdown => "text/x.cucumber.gherkin+markdown".to_json()
}
}
///|
test "location decode line only" {
let json = @json.parse("{\"line\": 1}") catch { _ => panic() }
let loc : Location = @json.from_json(json) catch { _ => panic() }
assert_eq(loc.line, 1)
assert_eq(loc.column, None)
}
///|
test "location decode line and column" {
let json = @json.parse("{\"line\": 2, \"column\": 3}") catch { _ => panic() }
let loc : Location = @json.from_json(json) catch { _ => panic() }
assert_eq(loc.line, 2)
assert_eq(loc.column, Some(3))
}
///|
test "location roundtrip" {
let loc = Location::{ line: 5, column: Some(10), }
let json = loc.to_json()
let back : Location = @json.from_json(json) catch { _ => panic() }
assert_eq(back.line, loc.line)
assert_eq(back.column, loc.column)
}
///|
test "timestamp roundtrip" {
let ts = Timestamp::{ seconds: 1234567890, nanos: 123456789, }
let json = ts.to_json()
let back : Timestamp = @json.from_json(json) catch { _ => panic() }
assert_eq(back.seconds, ts.seconds)
assert_eq(back.nanos, ts.nanos)
}
///|
test "duration roundtrip" {
let d = Duration::{ seconds: 5, nanos: 500_000_000, }
let json = d.to_json()
let back : Duration = @json.from_json(json) catch { _ => panic() }
assert_eq(back.seconds, d.seconds)
assert_eq(back.nanos, d.nanos)
}
///|
test "timestamp after 2038 parses and serializes seconds as a number" {
let json = @json.parse("{\"seconds\":4102444800,\"nanos\":5}")
let ts : Timestamp = @json.from_json(json)
assert_eq(ts.seconds, 4102444800L)
assert_eq(ts.nanos, 5)
assert_eq(ts.to_json().stringify(), "{\"seconds\":4102444800,\"nanos\":5}")
}
///|
test "duration with large seconds parses and serializes seconds as a number" {
let json = @json.parse("{\"seconds\":3000000000,\"nanos\":0}")
let d : Duration = @json.from_json(json)
assert_eq(d.seconds, 3000000000L)
assert_eq(d.to_json().stringify(), "{\"seconds\":3000000000,\"nanos\":0}")
}
///|
test "timestamp rejects fractional seconds" {
let json = @json.parse("{\"seconds\":1.5,\"nanos\":0}")
let ts : Timestamp? = Some(@json.from_json(json)) catch { _ => None }
assert_true(ts is None)
}