This time with the right formatting. I put tests in the same commit, since this will be added this way in master. Karchnu (1): Add use_cbor_discriminator (and its tests). spec/cbor_discrimination.cr | 67 +++++++++++++++++++++++++++++++++++++ src/cbor/decoder.cr | 13 +++++++ src/cbor/from_cbor.cr | 4 +++ src/cbor/lexer.cr | 7 ++++ src/cbor/serializable.cr | 30 +++++++++++++++++ 5 files changed, 121 insertions(+) create mode 100644 spec/cbor_discrimination.cr -- 2.30.2
crystal-cbor/patches/.build.yml: SUCCESS in 28s [use_cbor_discriminator: second rewrite][0] v2 from [~karchnu][1] [0]: https://lists.sr.ht/~arestifo/crystal-cbor/patches/23447 [1]: mailto:karchnu@karchnu.fr ✓ #531620 SUCCESS crystal-cbor/patches/.build.yml https://builds.sr.ht/~arestifo/job/531620
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.sr.ht/~arestifo/crystal-cbor/patches/23447/mbox | git am -3Learn more about email & git
From: Karchnu <karchnu@karchnu.fr> --- spec/cbor_discrimination.cr | 67 +++++++++++++++++++++++++++++++++++++ src/cbor/decoder.cr | 13 +++++++ src/cbor/from_cbor.cr | 4 +++ src/cbor/lexer.cr | 7 ++++ src/cbor/serializable.cr | 30 +++++++++++++++++ 5 files changed, 121 insertions(+) create mode 100644 spec/cbor_discrimination.cr diff --git a/spec/cbor_discrimination.cr b/spec/cbor_discrimination.cr new file mode 100644 index 0000000..0c438ad --- /dev/null +++ b/spec/cbor_discrimination.cr @@ -0,0 +1,67 @@ +require "./spec_helper" + +class Game + include CBOR::Serializable + + abstract class Player + include CBOR::Serializable + + @[CBOR::Field(key: "hp")] + property health_points : Int32 = 100 + + @[CBOR::Field(key: "dp")] + property defense_points : Int32 = 5 + + use_cbor_discriminator "type", { + magician: Magician, + warrior: Warrior, + } + + def initialize + end + end + + class Magician < Player + property wand_level : Int32 = 1 + + def initialize + @type = "magician" + end + end + + class Warrior < Player + def initialize + @defense_points = 20 + @type = "warrior" + end + end + + property players : Array(Magician | Warrior) + + def initialize + @players = [] of (Magician | Warrior) + end +end + +describe CBOR do + describe "Complex object representations" do + it "Game#to_cbor with use_cbor_discriminator" do + game = Game.new + magician = Game::Magician.new + magician.wand_level = 5 + game.players << magician + game.players << Game::Warrior.new + game.to_cbor.hexstring.should eq "a167706c617965727382a46268701864626470056a77616e645f6c6576656c056474797065686d6167696369616ea362687018646264701464747970656777617272696f72" + end + + it "Game#from_cbor with use_cbor_discriminator" do + game = Game.new + magician = Game::Magician.new + magician.wand_level = 5 + game.players << magician + game.players << Game::Warrior.new + new_game = Game.from_cbor game.to_cbor + new_game.to_cbor.hexstring.should eq "a167706c617965727382a46268701864626470056a77616e645f6c6576656c056474797065686d6167696369616ea362687018646264701464747970656777617272696f72" + end + end +end diff --git a/src/cbor/decoder.cr b/src/cbor/decoder.cr index 510fb45..117759f 100644 --- a/src/cbor/decoder.cr +++ b/src/cbor/decoder.cr @@ -2,6 +2,19 @@ class CBOR::Decoder @lexer : Lexer getter current_token : Token::T? + # Decode until a certain point in the history (use_cbor_discriminator helper). + def reset(value : Int32 | Int64 = 0) + @lexer.reset + while pos < value + @current_token = @lexer.next_token + end + end + + # Give the current position in the decoder (use_cbor_discriminator helper). + def pos + @lexer.io.pos + end + def initialize(input) @lexer = Lexer.new(input) @current_token = @lexer.next_token diff --git a/src/cbor/from_cbor.cr b/src/cbor/from_cbor.cr index e2610e7..0374c1f 100644 --- a/src/cbor/from_cbor.cr +++ b/src/cbor/from_cbor.cr @@ -3,6 +3,10 @@ def Object.from_cbor(string_or_io) new(parser) end +def Object.from_cbor(parser : CBOR::Decoder) + new(parser) +end + def String.new(decoder : CBOR::Decoder) decoder.read_string end diff --git a/src/cbor/lexer.cr b/src/cbor/lexer.cr index 8ba2a11..e469426 100644 --- a/src/cbor/lexer.cr +++ b/src/cbor/lexer.cr @@ -1,4 +1,6 @@ class CBOR::Lexer + property io : IO + def self.new(slice : Bytes) new IO::Memory.new(slice) end @@ -8,6 +10,11 @@ class CBOR::Lexer def initialize(@io : IO) end + def reset(value : Int32 | Int64 = 0) + @io.seek value + @eof = false + end + def next_token : Token::T? return nil if @eof diff --git a/src/cbor/serializable.cr b/src/cbor/serializable.cr index 4925470..1bd6d67 100644 --- a/src/cbor/serializable.cr +++ b/src/cbor/serializable.cr @@ -345,6 +345,36 @@ module CBOR end end + macro use_cbor_discriminator(field, mapping) + {% unless mapping.is_a?(HashLiteral) || mapping.is_a?(NamedTupleLiteral) %} + {% mapping.raise "mapping argument must be a HashLiteral or a NamedTupleLiteral, not #{mapping.class_name.id}" %} + {% end %} + + # SLOW. Read everything, get the type, read everything again. + def self.new(decoder : ::CBOR::Decoder) + current_offset = decoder.pos + if v = decoder.read_value + decoder.reset current_offset + case v + when Hash(CBOR::Type, CBOR::Type) + discriminator_value = v[{{field.id.stringify}}]? + case discriminator_value + {% for key, value in mapping %} + when {{key.id.stringify}} + return {{value.id}}.from_cbor(decoder) + {% end %} + else + raise "Unknown '{{field.id}}' discriminator value: #{discriminator_value.inspect}" + end + else + raise "cannot get cbor discriminator #{ {{ field.id.stringify }} }" + end + else + raise "cannot decode cbor value" + end + end + end + # Tells this class to decode CBOR by using a field as a discriminator. # # - *field* must be the field name to use as a discriminator -- 2.30.2
builds.sr.ht <builds@sr.ht>crystal-cbor/patches/.build.yml: SUCCESS in 28s [use_cbor_discriminator: second rewrite][0] v2 from [~karchnu][1] [0]: https://lists.sr.ht/~arestifo/crystal-cbor/patches/23447 [1]: mailto:karchnu@karchnu.fr ✓ #531620 SUCCESS crystal-cbor/patches/.build.yml https://builds.sr.ht/~arestifo/job/531620