diff --git a/spec/std/json/pull_parser_spec.cr b/spec/std/json/pull_parser_spec.cr index 35383dfbc435..48d0b74d0826 100644 --- a/spec/std/json/pull_parser_spec.cr +++ b/spec/std/json/pull_parser_spec.cr @@ -379,4 +379,15 @@ describe JSON::PullParser do pull.read?(Float32).should be_nil end end + + it "#raise" do + pull = JSON::PullParser.new("[1, 2, 3]") + expect_raises(JSON::ParseException, "foo bar at line 1, column 2") do + pull.raise "foo bar" + end + pull.read_begin_array + expect_raises(JSON::ParseException, "foo bar at line 1, column 3") do + pull.raise "foo bar" + end + end end diff --git a/spec/std/json/serialization_spec.cr b/spec/std/json/serialization_spec.cr index 72e5c47b6190..ae005fcfb64a 100644 --- a/spec/std/json/serialization_spec.cr +++ b/spec/std/json/serialization_spec.cr @@ -11,12 +11,14 @@ enum JSONSpecEnum Zero One Two + OneHundred end @[Flags] enum JSONSpecFlagEnum One Two + OneHundred end describe "JSON serialization" do @@ -212,30 +214,125 @@ describe "JSON serialization" do expect_raises(JSON::ParseException) { BigDecimal.from_json("{}") } end - it "does for Enum with number" do - JSONSpecEnum.from_json("1").should eq(JSONSpecEnum::One) + describe "Enum" do + it "normal enum" do + JSONSpecEnum.from_json(%("one")).should eq(JSONSpecEnum::One) + JSONSpecEnum.from_json(%("One")).should eq(JSONSpecEnum::One) + JSONSpecEnum.from_json(%("two")).should eq(JSONSpecEnum::Two) + JSONSpecEnum.from_json(%("ONE_HUNDRED")).should eq(JSONSpecEnum::OneHundred) + expect_raises(JSON::ParseException, %(Unknown enum JSONSpecEnum value: "ONE-HUNDRED")) do + JSONSpecEnum.from_json(%("ONE-HUNDRED")) + end + expect_raises(JSON::ParseException, %(Unknown enum JSONSpecEnum value: " one ")) do + JSONSpecEnum.from_json(%(" one ")) + end - expect_raises(Exception, "Unknown enum JSONSpecEnum value: 3") do - JSONSpecEnum.from_json("3") + expect_raises(JSON::ParseException, %(Unknown enum JSONSpecEnum value: "three")) do + JSONSpecEnum.from_json(%("three")) + end + expect_raises(JSON::ParseException, %(Expected String but was Int)) do + JSONSpecEnum.from_json(%(1)) + end + expect_raises(JSON::ParseException, %(Unknown enum JSONSpecEnum value: "1")) do + JSONSpecEnum.from_json(%("1")) + end + + expect_raises(JSON::ParseException, "Expected String but was BeginObject") do + JSONSpecEnum.from_json(%({})) + end + expect_raises(JSON::ParseException, "Expected String but was BeginArray") do + JSONSpecEnum.from_json(%([])) + end end - end - it "does for Enum with string" do - JSONSpecEnum.from_json(%("One")).should eq(JSONSpecEnum::One) + it "flag enum" do + JSONSpecFlagEnum.from_json(%(["one"])).should eq(JSONSpecFlagEnum::One) + JSONSpecFlagEnum.from_json(%(["One"])).should eq(JSONSpecFlagEnum::One) + JSONSpecFlagEnum.from_json(%(["one", "one"])).should eq(JSONSpecFlagEnum::One) + JSONSpecFlagEnum.from_json(%(["one", "two"])).should eq(JSONSpecFlagEnum::One | JSONSpecFlagEnum::Two) + JSONSpecFlagEnum.from_json(%(["one", "two", "one_hundred"])).should eq(JSONSpecFlagEnum::All) + JSONSpecFlagEnum.from_json(%([])).should eq(JSONSpecFlagEnum::None) + + expect_raises(JSON::ParseException, "Expected String but was BeginArray") do + JSONSpecFlagEnum.from_json(%(["one", ["two"]])) + end - expect_raises(ArgumentError, "Unknown enum JSONSpecEnum value: Three") do - JSONSpecEnum.from_json(%("Three")) + expect_raises(JSON::ParseException, %(Unknown enum JSONSpecFlagEnum value: "three")) do + JSONSpecFlagEnum.from_json(%(["one", "three"])) + end + expect_raises(JSON::ParseException, %(Expected String but was Int)) do + JSONSpecFlagEnum.from_json(%([1, 2])) + end + expect_raises(JSON::ParseException, %(Expected String but was Int)) do + JSONSpecFlagEnum.from_json(%(["one", 2])) + end + expect_raises(JSON::ParseException, "Expected BeginArray but was BeginObject") do + JSONSpecFlagEnum.from_json(%({})) + end + expect_raises(JSON::ParseException, "Expected BeginArray but was String") do + JSONSpecFlagEnum.from_json(%("one")) + end end end - it "does for flag Enum with number" do - JSONSpecFlagEnum.from_json("0").should eq(JSONSpecFlagEnum::None) - JSONSpecFlagEnum.from_json("1").should eq(JSONSpecFlagEnum::One) - JSONSpecFlagEnum.from_json("2").should eq(JSONSpecFlagEnum::Two) - JSONSpecFlagEnum.from_json("3").should eq(JSONSpecFlagEnum::All) + describe "Enum::ValueConverter.from_json" do + it "normal enum" do + Enum::ValueConverter(JSONSpecEnum).from_json("0").should eq(JSONSpecEnum::Zero) + Enum::ValueConverter(JSONSpecEnum).from_json("1").should eq(JSONSpecEnum::One) + Enum::ValueConverter(JSONSpecEnum).from_json("2").should eq(JSONSpecEnum::Two) + Enum::ValueConverter(JSONSpecEnum).from_json("3").should eq(JSONSpecEnum::OneHundred) + + expect_raises(JSON::ParseException, %(Expected Int but was String)) do + Enum::ValueConverter(JSONSpecEnum).from_json(%("3")) + end + expect_raises(JSON::ParseException, %(Unknown enum JSONSpecEnum value: 4)) do + Enum::ValueConverter(JSONSpecEnum).from_json("4") + end + expect_raises(JSON::ParseException, %(Unknown enum JSONSpecEnum value: -1)) do + Enum::ValueConverter(JSONSpecEnum).from_json("-1") + end + expect_raises(JSON::ParseException, %(Expected Int but was String)) do + Enum::ValueConverter(JSONSpecEnum).from_json(%("")) + end + + expect_raises(JSON::ParseException, "Expected Int but was String") do + Enum::ValueConverter(JSONSpecEnum).from_json(%("one")) + end + + expect_raises(JSON::ParseException, "Expected Int but was BeginObject") do + Enum::ValueConverter(JSONSpecEnum).from_json(%({})) + end + expect_raises(JSON::ParseException, "Expected Int but was BeginArray") do + Enum::ValueConverter(JSONSpecEnum).from_json(%([])) + end + end + + it "flag enum" do + Enum::ValueConverter(JSONSpecFlagEnum).from_json("0").should eq(JSONSpecFlagEnum::None) + Enum::ValueConverter(JSONSpecFlagEnum).from_json("1").should eq(JSONSpecFlagEnum::One) + Enum::ValueConverter(JSONSpecFlagEnum).from_json("2").should eq(JSONSpecFlagEnum::Two) + Enum::ValueConverter(JSONSpecFlagEnum).from_json("4").should eq(JSONSpecFlagEnum::OneHundred) + Enum::ValueConverter(JSONSpecFlagEnum).from_json("5").should eq(JSONSpecFlagEnum::OneHundred | JSONSpecFlagEnum::One) + Enum::ValueConverter(JSONSpecFlagEnum).from_json("7").should eq(JSONSpecFlagEnum::All) - expect_raises(Exception, "Unknown enum JSONSpecFlagEnum value: 4") do - JSONSpecFlagEnum.from_json("4") + expect_raises(JSON::ParseException, %(Unknown enum JSONSpecFlagEnum value: 8)) do + Enum::ValueConverter(JSONSpecFlagEnum).from_json("8") + end + expect_raises(JSON::ParseException, %(Unknown enum JSONSpecFlagEnum value: -1)) do + Enum::ValueConverter(JSONSpecFlagEnum).from_json("-1") + end + expect_raises(JSON::ParseException, %(Expected Int but was String)) do + Enum::ValueConverter(JSONSpecFlagEnum).from_json(%("")) + end + expect_raises(JSON::ParseException, "Expected Int but was String") do + Enum::ValueConverter(JSONSpecFlagEnum).from_json(%("one")) + end + expect_raises(JSON::ParseException, "Expected Int but was BeginObject") do + Enum::ValueConverter(JSONSpecFlagEnum).from_json(%({})) + end + expect_raises(JSON::ParseException, "Expected Int but was BeginArray") do + Enum::ValueConverter(JSONSpecFlagEnum).from_json(%([])) + end end end @@ -442,8 +539,74 @@ describe "JSON serialization" do {x: 1, y: "hello"}.to_json.should eq(%({"x":1,"y":"hello"})) end - it "does for Enum" do - JSONSpecEnum::One.to_json.should eq("1") + describe "Enum" do + it "normal enum" do + JSONSpecEnum::One.to_json.should eq %("one") + JSONSpecEnum.from_json(JSONSpecEnum::One.to_json).should eq(JSONSpecEnum::One) + + JSONSpecEnum::OneHundred.to_json.should eq %("one_hundred") + JSONSpecEnum.from_json(JSONSpecEnum::OneHundred.to_json).should eq(JSONSpecEnum::OneHundred) + + # undefined members can't be parsed back because the standard converter only accepts named + # members + JSONSpecEnum.new(42).to_json.should eq %("42") + end + + it "flag enum" do + JSONSpecFlagEnum::One.to_json.should eq %(["one"]) + JSONSpecFlagEnum.from_json(JSONSpecFlagEnum::One.to_json).should eq(JSONSpecFlagEnum::One) + + JSONSpecFlagEnum::OneHundred.to_json.should eq %(["one_hundred"]) + JSONSpecFlagEnum.from_json(JSONSpecFlagEnum::OneHundred.to_json).should eq(JSONSpecFlagEnum::OneHundred) + + combined = JSONSpecFlagEnum::OneHundred | JSONSpecFlagEnum::One + combined.to_json.should eq %(["one","one_hundred"]) + JSONSpecFlagEnum.from_json(combined.to_json).should eq(combined) + + JSONSpecFlagEnum::None.to_json.should eq %([]) + JSONSpecFlagEnum.from_json(JSONSpecFlagEnum::None.to_json).should eq(JSONSpecFlagEnum::None) + + JSONSpecFlagEnum::All.to_json.should eq %(["one","two","one_hundred"]) + JSONSpecFlagEnum.from_json(JSONSpecFlagEnum::All.to_json).should eq(JSONSpecFlagEnum::All) + + JSONSpecFlagEnum.new(42).to_json.should eq %(["two"]) + end + end + + describe "Enum::ValueConverter" do + it "normal enum" do + converter = Enum::ValueConverter(JSONSpecEnum) + converter.to_json(JSONSpecEnum::One).should eq %(1) + converter.from_json(converter.to_json(JSONSpecEnum::One)).should eq(JSONSpecEnum::One) + + converter.to_json(JSONSpecEnum::OneHundred).should eq %(3) + converter.from_json(converter.to_json(JSONSpecEnum::OneHundred)).should eq(JSONSpecEnum::OneHundred) + + # undefined members can't be parsed back because the standard converter only accepts named + # members + converter.to_json(JSONSpecEnum.new(42)).should eq %(42) + end + + it "flag enum" do + converter = Enum::ValueConverter(JSONSpecFlagEnum) + converter.to_json(JSONSpecFlagEnum::One).should eq %(1) + converter.from_json(converter.to_json(JSONSpecFlagEnum::One)).should eq(JSONSpecFlagEnum::One) + + converter.to_json(JSONSpecFlagEnum::OneHundred).should eq %(4) + converter.from_json(converter.to_json(JSONSpecFlagEnum::OneHundred)).should eq(JSONSpecFlagEnum::OneHundred) + + combined = JSONSpecFlagEnum::OneHundred | JSONSpecFlagEnum::One + converter.to_json(combined).should eq %(5) + converter.from_json(converter.to_json(combined)).should eq(combined) + + converter.to_json(JSONSpecFlagEnum::None).should eq %(0) + converter.from_json(converter.to_json(JSONSpecFlagEnum::None)).should eq(JSONSpecFlagEnum::None) + + converter.to_json(JSONSpecFlagEnum::All).should eq %(7) + converter.from_json(converter.to_json(JSONSpecFlagEnum::All)).should eq(JSONSpecFlagEnum::All) + + converter.to_json(JSONSpecFlagEnum.new(42)).should eq %(42) + end end pending_win32 "does for BigInt" do diff --git a/spec/std/yaml/serialization_spec.cr b/spec/std/yaml/serialization_spec.cr index f0303d93d8db..4f9c052ed222 100644 --- a/spec/std/yaml/serialization_spec.cr +++ b/spec/std/yaml/serialization_spec.cr @@ -9,25 +9,25 @@ enum YAMLSpecEnum Zero One Two + OneHundred end @[Flags] enum YAMLSpecFlagEnum One Two + OneHundred end alias YamlRec = Int32 | Array(YamlRec) | Hash(YamlRec, YamlRec) +puts YAML.libyaml_version + # libyaml 0.2.1 removed the erroneously written document end marker (`...`) after some scalars in root context (see https://github.com/yaml/libyaml/pull/18). # Earlier libyaml releases still write the document end marker and this is hard to fix on Crystal's side. # So we just ignore it and adopt the specs accordingly to coincide with the used libyaml version. -private def assert_yaml_document_end(actual, expected) - if YAML.libyaml_version < SemanticVersion.new(0, 2, 1) - expected += "...\n" - end - - actual.should eq(expected) +private def assert_yaml_document_end(actual, expected, file = __FILE__, line = __LINE__) + actual.rchop("...\n").should eq(expected), file: file, line: line end describe "YAML serialization" do @@ -186,30 +186,130 @@ describe "YAML serialization" do big.should eq(BigDecimal.new("1234.567891011121314")) end - it "does for Enum with number" do - YAMLSpecEnum.from_yaml(%("1")).should eq(YAMLSpecEnum::One) + describe "Enum" do + it "normal enum" do + YAMLSpecEnum.from_yaml(%("one")).should eq(YAMLSpecEnum::One) + YAMLSpecEnum.from_yaml(%("One")).should eq(YAMLSpecEnum::One) + YAMLSpecEnum.from_yaml(%("two")).should eq(YAMLSpecEnum::Two) + YAMLSpecEnum.from_yaml(%("ONE_HUNDRED")).should eq(YAMLSpecEnum::OneHundred) + expect_raises(YAML::ParseException, %(Unknown enum YAMLSpecEnum value: "ONE-HUNDRED")) do + YAMLSpecEnum.from_yaml(%("ONE-HUNDRED")) + end + expect_raises(YAML::ParseException, %(Unknown enum YAMLSpecEnum value: " one ")) do + YAMLSpecEnum.from_yaml(%(" one ")) + end + + expect_raises(YAML::ParseException, %(Unknown enum YAMLSpecEnum value: "three")) do + YAMLSpecEnum.from_yaml(%("three")) + end + expect_raises(YAML::ParseException, %(Expected String, not "1")) do + YAMLSpecEnum.from_yaml(%(1)) + end + expect_raises(YAML::ParseException, %(Unknown enum YAMLSpecEnum value: "1")) do + YAMLSpecEnum.from_yaml(%("1")) + end - expect_raises(Exception, "Unknown enum YAMLSpecEnum value: 3") do - YAMLSpecEnum.from_yaml(%("3")) + expect_raises(YAML::ParseException, "Expected scalar, not mapping") do + YAMLSpecEnum.from_yaml(%({})) + end + expect_raises(YAML::ParseException, "Expected scalar, not sequence") do + YAMLSpecEnum.from_yaml(%([])) + end end - end - it "does for Enum with string" do - YAMLSpecEnum.from_yaml(%("One")).should eq(YAMLSpecEnum::One) + it "flag enum" do + YAMLSpecFlagEnum.from_yaml(%(["one"])).should eq(YAMLSpecFlagEnum::One) + YAMLSpecFlagEnum.from_yaml(%(["One"])).should eq(YAMLSpecFlagEnum::One) + YAMLSpecFlagEnum.from_yaml(%([one])).should eq(YAMLSpecFlagEnum::One) + YAMLSpecFlagEnum.from_yaml(%(["one", "one"])).should eq(YAMLSpecFlagEnum::One) + YAMLSpecFlagEnum.from_yaml(%(["one", "two"])).should eq(YAMLSpecFlagEnum::One | YAMLSpecFlagEnum::Two) + YAMLSpecFlagEnum.from_yaml(%([one, two])).should eq(YAMLSpecFlagEnum::One | YAMLSpecFlagEnum::Two) + YAMLSpecFlagEnum.from_yaml(%(["one", "two", "one_hundred"])).should eq(YAMLSpecFlagEnum::All) + YAMLSpecFlagEnum.from_yaml(%([])).should eq(YAMLSpecFlagEnum::None) + + expect_raises(YAML::ParseException, "Expected scalar, not sequence") do + YAMLSpecFlagEnum.from_yaml(%(["one", ["two"]])) + end - expect_raises(ArgumentError, "Unknown enum YAMLSpecEnum value: Three") do - YAMLSpecEnum.from_yaml(%("Three")) + expect_raises(YAML::ParseException, %(Unknown enum YAMLSpecFlagEnum value: "three")) do + YAMLSpecFlagEnum.from_yaml(%(["one", "three"])) + end + expect_raises(YAML::ParseException, %(Expected String, not "1")) do + YAMLSpecFlagEnum.from_yaml(%([1, 2])) + end + expect_raises(YAML::ParseException, %(Expected String, not "2")) do + YAMLSpecFlagEnum.from_yaml(%(["one", 2])) + end + expect_raises(YAML::ParseException, "Expected sequence, not mapping") do + YAMLSpecFlagEnum.from_yaml(%({})) + end + expect_raises(YAML::ParseException, "Expected sequence, not scalar") do + YAMLSpecFlagEnum.from_yaml(%("one")) + end end end - it "does for flag Enum" do - YAMLSpecFlagEnum.from_json("0").should eq(YAMLSpecFlagEnum::None) - YAMLSpecFlagEnum.from_json("1").should eq(YAMLSpecFlagEnum::One) - YAMLSpecFlagEnum.from_json("2").should eq(YAMLSpecFlagEnum::Two) - YAMLSpecFlagEnum.from_json("3").should eq(YAMLSpecFlagEnum::All) + describe "Enum::ValueConverter.from_yaml" do + it "normal enum" do + Enum::ValueConverter(YAMLSpecEnum).from_yaml("0").should eq(YAMLSpecEnum::Zero) + Enum::ValueConverter(YAMLSpecEnum).from_yaml("1").should eq(YAMLSpecEnum::One) + Enum::ValueConverter(YAMLSpecEnum).from_yaml("2").should eq(YAMLSpecEnum::Two) + Enum::ValueConverter(YAMLSpecEnum).from_yaml("3").should eq(YAMLSpecEnum::OneHundred) + + expect_raises(YAML::ParseException, %(Expected Int64, not "3")) do + Enum::ValueConverter(YAMLSpecEnum).from_yaml(%("3")) + end + + expect_raises(YAML::ParseException, %(Unknown enum YAMLSpecEnum value: 4)) do + Enum::ValueConverter(YAMLSpecEnum).from_yaml("4") + end + expect_raises(YAML::ParseException, %(Unknown enum YAMLSpecEnum value: -1)) do + Enum::ValueConverter(YAMLSpecEnum).from_yaml("-1") + end + expect_raises(YAML::ParseException, %(Expected Int64, not )) do + Enum::ValueConverter(YAMLSpecEnum).from_yaml("") + end - expect_raises(Exception, "Unknown enum YAMLSpecFlagEnum value: 4") do - YAMLSpecFlagEnum.from_json("4") + expect_raises(YAML::ParseException, %(Expected Int64, not "one")) do + Enum::ValueConverter(YAMLSpecEnum).from_yaml(%("one")) + end + + expect_raises(YAML::ParseException, "Expected scalar, not mapping") do + Enum::ValueConverter(YAMLSpecEnum).from_yaml(%({})) + end + expect_raises(YAML::ParseException, "Expected scalar, not sequence") do + Enum::ValueConverter(YAMLSpecEnum).from_yaml(%([])) + end + end + + it "flag enum" do + Enum::ValueConverter(YAMLSpecFlagEnum).from_yaml("0").should eq(YAMLSpecFlagEnum::None) + Enum::ValueConverter(YAMLSpecFlagEnum).from_yaml("1").should eq(YAMLSpecFlagEnum::One) + Enum::ValueConverter(YAMLSpecFlagEnum).from_yaml("2").should eq(YAMLSpecFlagEnum::Two) + Enum::ValueConverter(YAMLSpecFlagEnum).from_yaml("4").should eq(YAMLSpecFlagEnum::OneHundred) + Enum::ValueConverter(YAMLSpecFlagEnum).from_yaml("5").should eq(YAMLSpecFlagEnum::OneHundred | YAMLSpecFlagEnum::One) + Enum::ValueConverter(YAMLSpecFlagEnum).from_yaml("7").should eq(YAMLSpecFlagEnum::All) + + expect_raises(YAML::ParseException, %(Unknown enum YAMLSpecFlagEnum value: 8)) do + Enum::ValueConverter(YAMLSpecFlagEnum).from_yaml("8") + end + expect_raises(YAML::ParseException, %(Unknown enum YAMLSpecFlagEnum value: -1)) do + Enum::ValueConverter(YAMLSpecFlagEnum).from_yaml("-1") + end + expect_raises(YAML::ParseException, %(Expected Int64, not "")) do + Enum::ValueConverter(YAMLSpecFlagEnum).from_yaml("") + end + + expect_raises(YAML::ParseException, %(Expected Int64, not "one")) do + Enum::ValueConverter(YAMLSpecFlagEnum).from_yaml(%("one")) + end + + expect_raises(YAML::ParseException, "Expected scalar, not mapping") do + Enum::ValueConverter(YAMLSpecFlagEnum).from_yaml(%({})) + end + expect_raises(YAML::ParseException, "Expected scalar, not sequence") do + Enum::ValueConverter(YAMLSpecFlagEnum).from_yaml(%([])) + end end end @@ -399,8 +499,74 @@ describe "YAML serialization" do BigDecimal.from_yaml(big.to_yaml).should eq(big) end - it "does for Enum" do - YAMLSpecEnum.from_yaml(YAMLSpecEnum::One.to_yaml).should eq(YAMLSpecEnum::One) + describe "Enum" do + it "normal enum" do + assert_yaml_document_end(YAMLSpecEnum::One.to_yaml, "--- one\n") + YAMLSpecEnum.from_yaml(YAMLSpecEnum::One.to_yaml).should eq(YAMLSpecEnum::One) + + assert_yaml_document_end(YAMLSpecEnum::OneHundred.to_yaml, "--- one_hundred\n") + YAMLSpecEnum.from_yaml(YAMLSpecEnum::OneHundred.to_yaml).should eq(YAMLSpecEnum::OneHundred) + + # undefined members can't be parsed back because the standard converter only accepts named + # members + assert_yaml_document_end(YAMLSpecEnum.new(42).to_yaml, %(--- "42"\n)) + end + + it "flag enum" do + assert_yaml_document_end(YAMLSpecFlagEnum::One.to_yaml, %(--- [one]\n)) + YAMLSpecFlagEnum.from_yaml(YAMLSpecFlagEnum::One.to_yaml).should eq(YAMLSpecFlagEnum::One) + + assert_yaml_document_end(YAMLSpecFlagEnum::OneHundred.to_yaml, %(--- [one_hundred]\n)) + YAMLSpecFlagEnum.from_yaml(YAMLSpecFlagEnum::OneHundred.to_yaml).should eq(YAMLSpecFlagEnum::OneHundred) + + combined = YAMLSpecFlagEnum::OneHundred | YAMLSpecFlagEnum::One + assert_yaml_document_end(combined.to_yaml, %(--- [one, one_hundred]\n)) + YAMLSpecFlagEnum.from_yaml(combined.to_yaml).should eq(combined) + + assert_yaml_document_end(YAMLSpecFlagEnum::None.to_yaml, %(--- []\n)) + YAMLSpecFlagEnum.from_yaml(YAMLSpecFlagEnum::None.to_yaml).should eq(YAMLSpecFlagEnum::None) + + assert_yaml_document_end(YAMLSpecFlagEnum::All.to_yaml, %(--- [one, two, one_hundred]\n)) + YAMLSpecFlagEnum.from_yaml(YAMLSpecFlagEnum::All.to_yaml).should eq(YAMLSpecFlagEnum::All) + + assert_yaml_document_end(YAMLSpecFlagEnum.new(42).to_yaml, "--- [two]\n") + end + end + + describe "Enum::ValueConverter" do + it "normal enum" do + converter = Enum::ValueConverter(YAMLSpecEnum) + assert_yaml_document_end(converter.to_yaml(YAMLSpecEnum::One), "--- 1\n") + converter.from_yaml(converter.to_yaml(YAMLSpecEnum::One)).should eq(YAMLSpecEnum::One) + + assert_yaml_document_end(converter.to_yaml(YAMLSpecEnum::OneHundred), "--- 3\n") + converter.from_yaml(converter.to_yaml(YAMLSpecEnum::OneHundred)).should eq(YAMLSpecEnum::OneHundred) + + # undefined members can't be parsed back because the standard converter only accepts named + # members + assert_yaml_document_end(converter.to_yaml(YAMLSpecEnum.new(42)), %(--- 42\n)) + end + + it "flag enum" do + converter = Enum::ValueConverter(YAMLSpecFlagEnum) + assert_yaml_document_end(converter.to_yaml(YAMLSpecFlagEnum::One), %(--- 1\n)) + converter.from_yaml(converter.to_yaml(YAMLSpecFlagEnum::One)).should eq(YAMLSpecFlagEnum::One) + + assert_yaml_document_end(converter.to_yaml(YAMLSpecFlagEnum::OneHundred), %(--- 4\n)) + converter.from_yaml(converter.to_yaml(YAMLSpecFlagEnum::OneHundred)).should eq(YAMLSpecFlagEnum::OneHundred) + + combined = YAMLSpecFlagEnum::OneHundred | YAMLSpecFlagEnum::One + assert_yaml_document_end(converter.to_yaml(combined), %(--- 5\n)) + converter.from_yaml(converter.to_yaml(combined)).should eq(combined) + + assert_yaml_document_end(converter.to_yaml(YAMLSpecFlagEnum::None), %(--- 0\n)) + converter.from_yaml(converter.to_yaml(YAMLSpecFlagEnum::None)).should eq(YAMLSpecFlagEnum::None) + + assert_yaml_document_end(converter.to_yaml(YAMLSpecFlagEnum::All), %(--- 7\n)) + converter.from_yaml(converter.to_yaml(YAMLSpecFlagEnum::All)).should eq(YAMLSpecFlagEnum::All) + + assert_yaml_document_end(converter.to_yaml(YAMLSpecFlagEnum.new(42)), "--- 42\n") + end end it "does for utc time" do diff --git a/src/json/from_json.cr b/src/json/from_json.cr index a9903648c785..8482b57c643d 100644 --- a/src/json/from_json.cr +++ b/src/json/from_json.cr @@ -243,14 +243,72 @@ def NamedTuple.new(pull : JSON::PullParser) {% end %} end +# Reads a serialized enum member by name from *pull*. +# +# See `#to_json` for reference. +# +# Raises `JSON::ParseException` if the deserialization fails. def Enum.new(pull : JSON::PullParser) - case pull.kind - when .int? - from_value(pull.read_int) - when .string? - parse(pull.read_string) - else - raise "Expecting int or string in JSON for #{self.class}, not #{pull.kind}" + {% if @type.annotation(Flags) %} + value = {{ @type }}::None + pull.read_array do + value |= parse?(pull.read_string) || pull.raise "Unknown enum #{self} value: #{pull.string_value.inspect}" + end + value + {% else %} + parse?(pull.read_string) || pull.raise "Unknown enum #{self} value: #{pull.string_value.inspect}" + {% end %} +end + +# Converter for value-based serialization and deserialization of enum type `T`. +# +# The serialization format of `Enum#to_json` and `Enum.from_json` is based on +# the member name. This converter offers an alternative based on the member value. +# +# This converter can be used for its standalone serialization methods as a +# replacement of the default strategy of `Enum`. It also works as a serialization +# converter with `JSON::Field` and `YAML::Field` +# +# ``` +# require "json" +# require "yaml" +# +# enum MyEnum +# ONE = 1 +# TWO = 2 +# end +# +# class Foo +# include JSON::Serializable +# include YAML::Serializable +# +# @[JSON::Field(converter: Enum::ValueConverter(MyEnum))] +# @[YAML::Field(converter: Enum::ValueConverter(MyEnum))] +# property foo : MyEnum = MyEnum::ONE +# end +# +# foo = Foo.new +# foo.to_json # => %({"my_enum":1}) +# foo.to_yaml # => %(---\nmy_enum: 1\n) +# ``` +# +# NOTE: Automatically assigned enum values are subject to change when the order +# of members by adding, removing or reordering them. This can affect the integrity +# of serialized data between two instances of a program based on different code +# versions. A way to avoid this is to explicitly assign fixed values to enum +# members. +module Enum::ValueConverter(T) + def self.new(pull : JSON::PullParser) : T + from_json(pull) + end + + # Reads a serialized enum member by value from *pull*. + # + # See `.to_json` for reference. + # + # Raises `JSON::ParseException` if the deserialization fails. + def self.from_json(pull : JSON::PullParser) : T + T.from_value?(pull.read_int) || pull.raise "Unknown enum #{T} value: #{pull.int_value}" end end diff --git a/src/json/pull_parser.cr b/src/json/pull_parser.cr index 82c17cdd113b..7f33412256e9 100644 --- a/src/json/pull_parser.cr +++ b/src/json/pull_parser.cr @@ -219,7 +219,7 @@ class JSON::PullParser when .float? @float_value.tap { read_next } else - parse_exception "expecting int or float but was #{@kind}" + raise "expecting int or float but was #{@kind}" end end @@ -679,20 +679,21 @@ class JSON::PullParser end private def expect_kind(kind : Kind) - parse_exception "Expected #{kind} but was #{@kind}" unless @kind == kind + raise "Expected #{kind} but was #{@kind}" unless @kind == kind end private def unexpected_token - parse_exception "Unexpected token: #{token}" + raise "Unexpected token: #{token}" end - private def parse_exception(msg) - raise ParseException.new(msg, token.line_number, token.column_number) + # Raises `ParseException` with *message* at current location. + def raise(message : String) + ::raise ParseException.new(message, token.line_number, token.column_number) end private def push_in_object_stack(kind : ObjectStackKind) if @object_stack.size >= @max_nesting - parse_exception "Nesting of #{@object_stack.size + 1} is too deep" + raise "Nesting of #{@object_stack.size + 1} is too deep" end @object_stack.push(kind) diff --git a/src/json/to_json.cr b/src/json/to_json.cr index e3e60dbdf08f..a6ea0ac04ae4 100644 --- a/src/json/to_json.cr +++ b/src/json/to_json.cr @@ -160,8 +160,97 @@ struct Time::Format end struct Enum + # Serializes this enum member by name. + # + # For non-flags enums, the serialization is a JSON string. The value is the + # member name (see `#to_s`) transformed with `String#underscore`. + # + # ``` + # enum Stages + # INITIAL + # SECOND_STAGE + # end + # + # Stages::INITIAL.to_json # => %("initial") + # Stages::SECOND_STAGE.to_json # => %("second_stage") + # ``` + # + # For flags enums, the serialization is a JSON array including every flagged + # member individually serialized in the same way as a member of a non-flags enum. + # `None` is serialized as an empty array, `All` as an array containing + # all members. + # + # ``` + # @[Flags] + # enum Sides + # LEFT + # RIGHT + # end + # + # Sides::LEFT.to_json # => %(["left"]) + # (Sides::LEFT | Sides::RIGHT).to_json # => %(["left", "right"]) + # Sides::All.to_json # => %(["left", "right"]) + # Sides::None.to_json # => %([]) + # ``` + # + # `ValueConverter.to_json` offers a different serialization strategy based on the + # member value. def to_json(json : JSON::Builder) - json.number(value) + {% if @type.annotation(Flags) %} + json.array do + each do |member, _value| + json.string(member.to_s.underscore) + end + end + {% else %} + json.string(to_s.underscore) + {% end %} + end +end + +module Enum::ValueConverter(T) + def self.to_json(value : T) + String.build do |io| + to_json(value, io) + end + end + + def self.to_json(value : T, io : IO) + JSON.build(io) do |json| + to_json(value, json) + end + end + + # Serializes enum member *member* by value. + # + # For both flags enums and non-flags enums, the value of the enum member is + # used for serialization. + # + # ``` + # enum Stages + # INITIAL + # SECOND_STAGE + # end + # + # Enum::ValueConverter.to_json(Stages::INITIAL) # => %(0) + # Enum::ValueConverter.to_json(Stages::SECOND_STAGE) # => %(1) + # + # @[Flags] + # enum Sides + # LEFT + # RIGHT + # end + # + # Enum::ValueConverter.to_json(Sides::LEFT) # => %(1) + # Enum::ValueConverter.to_json(Sides::LEFT | Sides::RIGHT) # => %(3) + # Enum::ValueConverter.to_json(Sides::All) # => %(3) + # Enum::ValueConverter.to_json(Sides::None) # => %(0) + # ``` + # + # `Enum#to_json` offers a different serialization strategy based on the member + # name. + def self.to_json(member : T, json : JSON::Builder) + json.scalar(member.value) end end diff --git a/src/yaml/from_yaml.cr b/src/yaml/from_yaml.cr index 5509c5f1e572..abea6ab054a8 100644 --- a/src/yaml/from_yaml.cr +++ b/src/yaml/from_yaml.cr @@ -219,16 +219,45 @@ def NamedTuple.new(ctx : YAML::ParseContext, node : YAML::Nodes::Node) {% end %} end +# Reads a serialized enum member by name from *ctx* and *node*. +# +# See `#to_yaml` for reference. +# +# Raises `YAML::ParseException` if the deserialization fails. def Enum.new(ctx : YAML::ParseContext, node : YAML::Nodes::Node) - unless node.is_a?(YAML::Nodes::Scalar) - node.raise "Expected scalar, not #{node.kind}" + {% if @type.annotation(Flags) %} + if node.is_a?(YAML::Nodes::Sequence) + value = {{ @type }}::None + node.each do |element| + string = parse_scalar(ctx, element, String) + + value |= parse?(string) || element.raise "Unknown enum #{self} value: #{string.inspect}" + end + + value + else + node.raise "Expected sequence, not #{node.kind}" + end + {% else %} + string = parse_scalar(ctx, node, String) + parse?(string) || node.raise "Unknown enum #{self} value: #{string.inspect}" + {% end %} +end + +module Enum::ValueConverter(T) + def self.new(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : T + from_yaml(ctx, node) end - string = node.value - if value = string.to_i64? - from_value(value) - else - parse(string) + # Reads a serialized enum member by value from *ctx* and *node*. + # + # See `.to_yaml` for reference. + # + # Raises `YAML::ParseException` if the deserialization fails. + def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : T + value = parse_scalar ctx, node, Int64 + + T.from_value?(value) || node.raise "Unknown enum #{T} value: #{value}" end end diff --git a/src/yaml/to_yaml.cr b/src/yaml/to_yaml.cr index 66aefd7630da..651072a0b83f 100644 --- a/src/yaml/to_yaml.cr +++ b/src/yaml/to_yaml.cr @@ -122,8 +122,101 @@ struct Symbol end struct Enum + # Serializes this enum member by name. + # + # For non-flags enums, the serialization is a YAML string. The value is the + # member name (see `#to_s`) transformed with `String#underscore`. + # + # ``` + # enum Stages + # INITIAL + # SECOND_STAGE + # end + # + # Stages::INITIAL.to_yaml # => %(--- initial\n) + # Stages::SECOND_STAGE.to_yaml # => %(--- second_stage\n) + # ``` + # + # For flags enums, the serialization is a YAML sequence including every flagged + # member individually serialized in the same way as a member of a non-flags enum. + # `None` is serialized as an empty sequence, `All` as a sequence containing + # all members. + # + # ``` + # @[Flags] + # enum Sides + # LEFT + # RIGHT + # end + # + # Sides::LEFT.to_yaml # => %(--- ["left"]\n) + # (Sides::LEFT | Sides::RIGHT).to_yaml # => %(--- ["left", "right"]\n) + # Sides::All.to_yaml # => %(--- ["left", "right"]\n) + # Sides::None.to_yaml # => %(--- []\n) + # ``` + # + # `ValueConverter.to_yaml` offers a different serialization strategy based on the + # member value. def to_yaml(yaml : YAML::Nodes::Builder) - yaml.scalar value + {% if @type.annotation(Flags) %} + yaml.sequence(style: :flow) do + each do |member, _value| + member.to_s.underscore.to_yaml(yaml) + end + end + {% else %} + to_s.underscore.to_yaml(yaml) + {% end %} + end +end + +module Enum::ValueConverter(T) + def self.to_yaml(value : T) + String.build do |io| + to_yaml(value, io) + end + end + + def self.to_yaml(value : T, io : IO) + nodes_builder = YAML::Nodes::Builder.new + to_yaml(value, nodes_builder) + + # Then we convert the tree to YAML. + YAML.build(io) do |builder| + nodes_builder.document.to_yaml(builder) + end + end + + # Serializes enum member *member* by value. + # + # For both flags enums and non-flags enums, the value of the enum member is + # used for serialization. + # + # ``` + # enum Stages + # INITIAL + # SECOND_STAGE + # end + # + # Enum::ValueConverter.to_yaml(Stages::INITIAL) # => %(0) + # Enum::ValueConverter.to_yaml(Stages::SECOND_STAGE) # => %(1) + # + # @[Flags] + # enum Sides + # LEFT + # RIGHT + # end + # + # Enum::ValueConverter.to_yaml(Sides::LEFT) # => %(1) + # Enum::ValueConverter.to_yaml(Sides::LEFT | Sides::RIGHT) # => %(3) + # Enum::ValueConverter.to_yaml(Sides::All) # => %(3) + # Enum::ValueConverter.to_yaml(Sides::None) # => %(0) + # ``` + # + # `Enum#to_yaml` offers a different serialization strategy based on the member + # name. + def self.to_yaml(member : T, yaml : YAML::Nodes::Builder) + yaml.scalar(member.value) end end