diff --git a/Guardfile b/Guardfile index 0bdc219..e37fabb 100644 --- a/Guardfile +++ b/Guardfile @@ -1,6 +1,7 @@ +# frozen_string_literal: true # More info at https://github.com/guard/guard#readme -guard :rspec, :cmd => "GUARD_RSPEC=1 bundle exec rspec --no-profile" do +guard :rspec, cmd: "GUARD_RSPEC=1 bundle exec rspec --no-profile" do require "guard/rspec/dsl" dsl = Guard::RSpec::Dsl.new(self) diff --git a/lib/tjson.rb b/lib/tjson.rb index db1922a..494c62a 100644 --- a/lib/tjson.rb +++ b/lib/tjson.rb @@ -7,11 +7,10 @@ require "base32" require "base64" -require "tjson/array" require "tjson/binary" +require "tjson/datatype" require "tjson/generator" require "tjson/object" -require "tjson/parser" # Tagged JSON with Rich Types module TJSON @@ -24,6 +23,9 @@ module TJSON # Failure to parse TJSON document ParseError = Class.new(Error) + # Invalid types + TypeError = Class.new(ParseError) + # Duplicate object name DuplicateNameError = Class.new(ParseError) @@ -43,18 +45,20 @@ def self.parse(string) end begin - ::JSON.parse( + object = ::JSON.parse( utf8_string, max_nesting: MAX_NESTING, allow_nan: false, symbolize_names: false, create_additions: false, - object_class: TJSON::Object, - array_class: TJSON::Array + object_class: TJSON::Object ) rescue ::JSON::ParserError => ex raise TJSON::ParseError, ex.message, ex.backtrace end + + raise TJSON::TypeError, "invalid toplevel type: #{object.class}" unless object.is_a?(TJSON::Object) + object end # Generate TJSON from a Ruby Hash or Array diff --git a/lib/tjson/array.rb b/lib/tjson/array.rb deleted file mode 100644 index 3cb7c53..0000000 --- a/lib/tjson/array.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -module TJSON - # TJSON array type - class Array < ::Array - def <<(obj) - super(TJSON::Parser.value(obj)) - end - end -end diff --git a/lib/tjson/datatype.rb b/lib/tjson/datatype.rb new file mode 100644 index 0000000..e873a3d --- /dev/null +++ b/lib/tjson/datatype.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module TJSON + # Hierarchy of TJSON types + class DataType + # Find a type by its tag + def self.[](tag) + TAGS[tag] || raise(TJSON::TypeError, "unknown tag: #{tag.inspect}") + end + + def self.parse(tag) + raise TJSON::TypeError, "expected String, got #{tag.class}" unless tag.is_a?(::String) + + if tag == "O" + # Object + TJSON::DataType[tag] + elsif (result = tag.match(/\A(?[A-Z][a-z0-9]*)\<(?.*)\>\z/)) + # Non-scalar + TJSON::DataType[result[:type]].new(parse(result[:inner])).freeze + elsif tag =~ /\A[a-z][a-z0-9]*\z/ + # Scalar + TJSON::DataType[tag] + else + raise TJSON::ParseError, "couldn't parse tag: #{tag.inspect}" unless result + end + end + end +end + +require "tjson/datatype/nonscalar" +require "tjson/datatype/scalar" + +require "tjson/datatype/binary" +require "tjson/datatype/number" +require "tjson/datatype/string" +require "tjson/datatype/timestamp" + +# TJSON does not presently support user-extensible types +TJSON::DataType::TAGS = { + # Object (non-scalar with self-describing types) + "O" => TJSON::DataType::Object.new(nil).freeze, + + # Non-scalars + "A" => TJSON::DataType::Array, + + # Scalars + "b" => TJSON::DataType::Binary64.new.freeze, + "b16" => TJSON::DataType::Binary16.new.freeze, + "b32" => TJSON::DataType::Binary32.new.freeze, + "b64" => TJSON::DataType::Binary64.new.freeze, + "f" => TJSON::DataType::Float.new.freeze, + "i" => TJSON::DataType::SignedInt.new.freeze, + "s" => TJSON::DataType::UnicodeString.new.freeze, + "t" => TJSON::DataType::Timestamp.new.freeze, + "u" => TJSON::DataType::UnsignedInt.new.freeze +}.freeze diff --git a/lib/tjson/datatype/binary.rb b/lib/tjson/datatype/binary.rb new file mode 100644 index 0000000..65c54c9 --- /dev/null +++ b/lib/tjson/datatype/binary.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module TJSON + class DataType + # Binary Data + class Binary < Scalar; end + + # Base16-serialized binary data + class Binary16 < Binary + def convert(str) + raise TJSON::TypeError, "expected String, got #{str.class}: #{str.inspect}" unless str.is_a?(::String) + raise TJSON::ParseError, "base16 must be lower case: #{str.inspect}" if str =~ /[A-F]/ + raise TJSON::ParseError, "invalid base16: #{str.inspect}" unless str =~ /\A[a-f0-9]*\z/ + + [str].pack("H*") + end + end + + # Base32-serialized binary data + class Binary32 < Binary + def convert(str) + raise TJSON::TypeError, "expected String, got #{str.class}: #{str.inspect}" unless str.is_a?(::String) + raise TJSON::ParseError, "base32 must be lower case: #{str.inspect}" if str =~ /[A-Z]/ + raise TJSON::ParseError, "padding disallowed: #{str.inspect}" if str.include?("=") + raise TJSON::ParseError, "invalid base32: #{str.inspect}" unless str =~ /\A[a-z2-7]*\z/ + + ::Base32.decode(str.upcase).force_encoding(Encoding::BINARY) + end + end + + # Base64-serialized binary data + class Binary64 < Binary + def convert(str) + raise TJSON::TypeError, "expected String, got #{str.class}: #{str.inspect}" unless str.is_a?(::String) + raise TJSON::ParseError, "base64url only: #{str.inspect}" if str =~ %r{\+|\/} + raise TJSON::ParseError, "padding disallowed: #{str.inspect}" if str.include?("=") + raise TJSON::ParseError, "invalid base64url: #{str.inspect}" unless str =~ /\A[A-Za-z0-9\-_]*\z/ + + # Add padding, as older Rubies (< 2.3) require it + str = str.ljust((str.length + 3) & ~3, "=") if (str.length % 4).nonzero? + + ::Base64.urlsafe_decode64(str) + end + end + end +end diff --git a/lib/tjson/datatype/nonscalar.rb b/lib/tjson/datatype/nonscalar.rb new file mode 100644 index 0000000..85255dd --- /dev/null +++ b/lib/tjson/datatype/nonscalar.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module TJSON + class DataType + # Non-scalar types + class NonScalar < TJSON::DataType + attr_reader :inner_type + + def initialize(inner_type) + @inner_type = inner_type + end + + def inspect + "#<#{self.class}<#{@inner_type.inspect}>>" + end + + def scalar? + false + end + end + + # TJSON arrays + class Array < NonScalar + def convert(array) + raise TJSON::TypeError, "expected Array, got #{array.class}" unless array.is_a?(::Array) + array.map! { |o| @inner_type.convert(o) } + end + end + + # TJSON objects + class Object < NonScalar + def convert(obj) + raise TJSON::TypeError, "expected TJSON::Object, got #{obj.class}" unless obj.is_a?(TJSON::Object) + + # Objects handle their own member conversions + obj + end + + def inspect + "#<#{self.class}>" + end + end + end +end diff --git a/lib/tjson/datatype/number.rb b/lib/tjson/datatype/number.rb new file mode 100644 index 0000000..e0b8813 --- /dev/null +++ b/lib/tjson/datatype/number.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module TJSON + class DataType + # Numbers + class Number < Scalar; end + class Integer < Scalar; end + + # Floating point type + class Float < Number + def convert(float) + raise TJSON::TypeError, "expected Float, got #{float.class}" unless float.is_a?(::Numeric) + float.to_f + end + end + + # Signed 64-bit integer + class SignedInt < Integer + def convert(str) + raise TJSON::TypeError, "expected String, got #{str.class}: #{str.inspect}" unless str.is_a?(::String) + raise TJSON::ParseError, "invalid integer: #{str.inspect}" unless str =~ /\A\-?(0|[1-9][0-9]*)\z/ + + result = Integer(str, 10) + raise TJSON::ParseError, "oversized integer: #{result}" if result > 9_223_372_036_854_775_807 + raise TJSON::ParseError, "undersized integer: #{result}" if result < -9_223_372_036_854_775_808 + + result + end + end + + # Unsigned 64-bit integer + class UnsignedInt < Integer + def convert(str) + raise TJSON::TypeError, "expected String, got #{str.class}: #{str.inspect}" unless str.is_a?(::String) + raise TJSON::ParseError, "invalid integer: #{str.inspect}" unless str =~ /\A(0|[1-9][0-9]*)\z/ + + result = Integer(str, 10) + raise TJSON::ParseError, "oversized integer: #{result}" if result > 18_446_744_073_709_551_615 + + result + end + end + end +end diff --git a/lib/tjson/datatype/scalar.rb b/lib/tjson/datatype/scalar.rb new file mode 100644 index 0000000..044ff0d --- /dev/null +++ b/lib/tjson/datatype/scalar.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module TJSON + class DataType + # Scalar types + class Scalar < TJSON::DataType + def scalar? + true + end + + def inspect + "#<#{self.class}>" + end + end + end +end diff --git a/lib/tjson/datatype/string.rb b/lib/tjson/datatype/string.rb new file mode 100644 index 0000000..ebae650 --- /dev/null +++ b/lib/tjson/datatype/string.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module TJSON + class DataType + # Unicode String type + class UnicodeString < Scalar + def convert(str) + raise TJSON::TypeError, "expected String, got #{str.class}: #{str.inspect}" unless str.is_a?(::String) + raise TJSON::EncodingError, "expected UTF-8, got #{str.encoding.inspect}" unless str.encoding == Encoding::UTF_8 + str + end + end + end +end diff --git a/lib/tjson/datatype/timestamp.rb b/lib/tjson/datatype/timestamp.rb new file mode 100644 index 0000000..3816761 --- /dev/null +++ b/lib/tjson/datatype/timestamp.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module TJSON + class DataType + # RFC3339 timestamp (Z-normalized) + class Timestamp < Scalar + def convert(str) + raise TJSON::TypeError, "expected String, got #{str.class}: #{str.inspect}" unless str.is_a?(::String) + raise TJSON::ParseError, "invalid timestamp: #{str.inspect}" unless str =~ /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z\z/ + + ::Time.iso8601(str) + end + end + end +end diff --git a/lib/tjson/object.rb b/lib/tjson/object.rb index b3036f6..1ff4380 100644 --- a/lib/tjson/object.rb +++ b/lib/tjson/object.rb @@ -3,16 +3,15 @@ module TJSON # TJSON object type (i.e. hash/dict-alike) class Object < ::Hash - def []=(name, value) - unless name.start_with?("s:", "b16:", "b64:") - raise TJSON::ParseError, "invalid tag on object name: #{name[/\A(.*?):/, 1]}" if name.include?(":") - raise TJSON::ParseError, "no tag found on object name: #{name.inspect}" - end + def []=(tagged_name, value) + # NOTE: this regex is sloppy. The real parsing is performed in TJSON::DataType#parse + result = tagged_name.match(/\A(?.*):(?[A-Za-z0-9\<]+[\>]*)\z/) - name = TJSON::Parser.value(name) - raise TJSON::DuplicateNameError, "duplicate member name: #{name.inspect}" if key?(name) + raise ParseError, "invalid tag: #{tagged_name.inspect}" unless result + raise DuplicateNameError, "duplicate member name: #{result[:name].inspect}" if key?(result[:name]) - super(name, TJSON::Parser.value(value)) + type = TJSON::DataType.parse(result[:tag]) + super(result[:name], type.convert(value)) end end end diff --git a/lib/tjson/parser.rb b/lib/tjson/parser.rb deleted file mode 100644 index 317111b..0000000 --- a/lib/tjson/parser.rb +++ /dev/null @@ -1,97 +0,0 @@ -# frozen_string_literal: true - -module TJSON - # Postprocessing for extracting TJSON tags from JSON - module Parser - module_function - - TAG_DELIMITER = ":" - MAX_TAG_LENGTH = 3 # Sans ':' - - def value(obj) - case obj - when String then parse(obj) - when Integer then obj.to_f - when TJSON::Object, TJSON::Array, Float then obj - else raise TypeError, "invalid TJSON value: #{obj.inspect}" - end - end - - def parse(str) - raise TypeError, "expected String, got #{str.class}" unless str.is_a?(::String) - raise TJSON::EncodingError, "expected UTF-8, got #{str.encoding.inspect}" unless str.encoding == Encoding::UTF_8 - - dpos = str.index(TAG_DELIMITER) - - raise TJSON::ParseError, "invalid tag (missing ':' delimiter)" unless dpos - raise TJSON::ParseError, "overlength tag (maximum #{MAX_TAG_LENGTH})" if dpos > MAX_TAG_LENGTH - - tag = str.slice!(0, dpos + 1) - - case tag - when "s:" then str - when "i:" then parse_signed_int(str) - when "u:" then parse_unsigned_int(str) - when "t:" then parse_timestamp(str) - when "b16:" then parse_base16(str) - when "b32:" then parse_base32(str) - when "b64:" then parse_base64url(str) - else raise TJSON::ParseError, "invalid tag #{tag.inspect} on string #{str.inspect}" - end - end - - def parse_base16(str) - raise TypeError, "expected String, got #{str.class}" unless str.is_a?(::String) - raise TJSON::ParseError, "base16 must be lower case: #{str.inspect}" if str =~ /[A-F]/ - raise TJSON::ParseError, "invalid base16: #{str.inspect}" unless str =~ /\A[a-f0-9]*\z/ - - [str].pack("H*") - end - - def parse_base32(str) - raise TypeError, "expected String, got #{str.class}" unless str.is_a?(::String) - raise TJSON::ParseError, "base32 must be lower case: #{str.inspect}" if str =~ /[A-Z]/ - raise TJSON::ParseError, "padding disallowed: #{str.inspect}" if str.include?("=") - raise TJSON::ParseError, "invalid base32: #{str.inspect}" unless str =~ /\A[a-z2-7]*\z/ - - Base32.decode(str.upcase).force_encoding(Encoding::BINARY) - end - - def parse_base64url(str) - raise TypeError, "expected String, got #{str.class}" unless str.is_a?(::String) - raise TJSON::ParseError, "base64url only: #{str.inspect}" if str =~ %r{\+|\/} - raise TJSON::ParseError, "padding disallowed: #{str.inspect}" if str.include?("=") - raise TJSON::ParseError, "invalid base64url: #{str.inspect}" unless str =~ /\A[A-Za-z0-9\-_]*\z/ - - # Add padding, as older Rubies (< 2.3) require it - str = str.ljust((str.length + 3) & ~3, "=") if (str.length % 4).nonzero? - - Base64.urlsafe_decode64(str) - end - - def parse_signed_int(str) - raise TJSON::ParseError, "invalid integer: #{str.inspect}" unless str =~ /\A\-?(0|[1-9][0-9]*)\z/ - - result = Integer(str, 10) - raise TJSON::ParseError, "oversized integer: #{result}" if result > 9_223_372_036_854_775_807 - raise TJSON::ParseError, "undersized integer: #{result}" if result < -9_223_372_036_854_775_808 - - result - end - - def parse_unsigned_int(str) - raise TJSON::ParseError, "invalid integer: #{str.inspect}" unless str =~ /\A(0|[1-9][0-9]*)\z/ - - result = Integer(str, 10) - raise TJSON::ParseError, "oversized integer: #{result}" if result > 18_446_744_073_709_551_615 - - result - end - - def parse_timestamp(str) - raise TJSON::ParseError, "invalid timestamp: #{str.inspect}" unless str =~ /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z\z/ - - Time.iso8601(str) - end - end -end diff --git a/spec/tjson/buffer_spec.rb b/spec/tjson/buffer_spec.rb deleted file mode 100644 index d6cf3e0..0000000 --- a/spec/tjson/buffer_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe TJSON::Binary do - let(:example_string) { "Hello, world!" } - - describe ".base16" do - it "serializes base16" do - expect(described_class.base16(example_string)).to eq "b16:48656c6c6f2c20776f726c6421" - end - end - - describe ".base32" do - it "serializes base32" do - expect(described_class.base32(example_string)).to eq "b32:jbswy3dpfqqho33snrscc" - end - end - - describe ".base64" do - it "serializes base64url" do - expect(described_class.base64(example_string)).to eq "b64:SGVsbG8sIHdvcmxkIQ" - end - end -end diff --git a/spec/tjson/datatype/binary_spec.rb b/spec/tjson/datatype/binary_spec.rb new file mode 100644 index 0000000..b746c1d --- /dev/null +++ b/spec/tjson/datatype/binary_spec.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +RSpec.describe TJSON::DataType::Binary do + describe TJSON::DataType::Binary16 do + subject(:binary16) { described_class.new } + + context "valid base16 string" do + let(:example_base16) { "48656c6c6f2c20776f726c6421" } + let(:example_result) { "Hello, world!" } + + it "parses successfully" do + result = binary16.convert(example_base16) + expect(result).to eq example_result + expect(result.encoding).to eq Encoding::BINARY + end + end + + context "invalid base16 string" do + let(:invalid_base16) { "Surely this is not valid base16!" } + + it "raises TJSON::ParseError" do + expect { binary16.convert(invalid_base16) }.to raise_error(TJSON::ParseError) + end + end + end + + describe TJSON::DataType::Binary32 do + subject(:binary32) { described_class.new } + + context "valid base32 string" do + let(:example_base32) { "jbswy3dpfqqho33snrscc" } + let(:example_result) { "Hello, world!" } + + it "parses successfully" do + result = binary32.convert(example_base32) + expect(result).to eq example_result + expect(result.encoding).to eq Encoding::BINARY + end + end + + context "padded base32 string" do + let(:padded_base32) { "jbswy3dpfqqho33snrscc===" } + + it "raises TJSON::ParseError" do + expect { binary32.convert(padded_base32) }.to raise_error(TJSON::ParseError) + end + end + + context "invalid base32 string" do + let(:invalid_base32) { "Surely this is not valid base32!" } + + it "raises TJSON::ParseError" do + expect { binary32.convert(invalid_base32) }.to raise_error(TJSON::ParseError) + end + end + end + + describe TJSON::DataType::Binary64 do + subject(:binary64) { described_class.new } + + context "valid base64url string" do + let(:example_base64url) { "SGVsbG8sIHdvcmxkIQ" } + let(:example_result) { "Hello, world!" } + + it "parses successfully" do + result = binary64.convert(example_base64url) + expect(result).to eq example_result + expect(result.encoding).to eq Encoding::BINARY + end + end + + context "padded base64url string" do + let(:padded_base64url) { "SGVsbG8sIHdvcmxkIQ==" } + + it "raises TJSON::ParseError" do + expect { binary64.convert(padded_base64url) }.to raise_error(TJSON::ParseError) + end + end + + context "invalid base64url string" do + let(:invalid_base64url) { "Surely this is not valid base64url!" } + + it "raises TJSON::ParseError" do + expect { binary64.convert(invalid_base64url) }.to raise_error(TJSON::ParseError) + end + end + end +end diff --git a/spec/tjson/datatype/nonscalar_spec.rb b/spec/tjson/datatype/nonscalar_spec.rb new file mode 100644 index 0000000..5fbbd67 --- /dev/null +++ b/spec/tjson/datatype/nonscalar_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +RSpec.describe TJSON::DataType::NonScalar do + subject(:nonscalar) { described_class.new(nil) } + + it "knows it's not a scalar" do + expect(nonscalar).not_to be_scalar + end + + describe TJSON::DataType::Array do + it "needs tests!" + end + + describe TJSON::DataType::Object do + it "needs tests!" + end +end diff --git a/spec/tjson/datatype/number_spec.rb b/spec/tjson/datatype/number_spec.rb new file mode 100644 index 0000000..57fbc88 --- /dev/null +++ b/spec/tjson/datatype/number_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +RSpec.describe TJSON::DataType::Number do + describe TJSON::DataType::Float do + it "needs tests!" + end + + describe TJSON::DataType::SignedInt do + it "needs tests!" + end + + describe TJSON::DataType::UnsignedInt do + it "needs tests!" + end +end diff --git a/spec/tjson/datatype/scalar_spec.rb b/spec/tjson/datatype/scalar_spec.rb new file mode 100644 index 0000000..e00a419 --- /dev/null +++ b/spec/tjson/datatype/scalar_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +RSpec.describe TJSON::DataType::Scalar do + subject(:scalar) { described_class.new } + + it "knows it's a scalar" do + expect(scalar).to be_scalar + end +end diff --git a/spec/tjson/datatype/string_spec.rb b/spec/tjson/datatype/string_spec.rb new file mode 100644 index 0000000..e69de29 diff --git a/spec/tjson/array_spec.rb b/spec/tjson/datatype/tiemstamp_spec.rb similarity index 55% rename from spec/tjson/array_spec.rb rename to spec/tjson/datatype/tiemstamp_spec.rb index 31a599f..0a6a650 100644 --- a/spec/tjson/array_spec.rb +++ b/spec/tjson/datatype/tiemstamp_spec.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -RSpec.describe TJSON::Array do +RSpec.describe TJSON::DataType::Timestamp do it "needs tests!" end diff --git a/spec/tjson/datatype_spec.rb b/spec/tjson/datatype_spec.rb new file mode 100644 index 0000000..39efcc1 --- /dev/null +++ b/spec/tjson/datatype_spec.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +RSpec.describe TJSON::DataType do + context "scalars" do + context "binary data" do + it "parses base16 tags" do + expect(described_class.parse("b16")).to be_a TJSON::DataType::Binary16 + end + + it "parses base32 tags" do + expect(described_class.parse("b32")).to be_a TJSON::DataType::Binary32 + end + + it "parses base64url tags" do + expect(described_class.parse("b64")).to be_a TJSON::DataType::Binary64 + end + + it "parses implicit base64url tags" do + expect(described_class.parse("b")).to be_a TJSON::DataType::Binary64 + end + end + + context "numbers" do + it "parses float tags" do + expect(described_class.parse("f")).to be_a TJSON::DataType::Float + end + + it "parses signed integer tags" do + expect(described_class.parse("i")).to be_a TJSON::DataType::SignedInt + end + + it "parses unsigned integer tags" do + expect(described_class.parse("u")).to be_a TJSON::DataType::UnsignedInt + end + end + + it "parses string tags" do + expect(described_class.parse("s")).to be_a TJSON::DataType::UnicodeString + end + + it "parses timestamp tags" do + expect(described_class.parse("t")).to be_a TJSON::DataType::Timestamp + end + end + + context "nonscalars" do + it "parses array tags" do + result = described_class.parse("A") + expect(result).to be_a TJSON::DataType::Array + expect(result.inner_type).to be_a TJSON::DataType::SignedInt + end + + it "parses nested array tags" do + result = described_class.parse("A>") + expect(result).to be_a TJSON::DataType::Array + + inner = result.inner_type + expect(inner).to be_a TJSON::DataType::Array + expect(inner.inner_type).to be_a TJSON::DataType::SignedInt + end + + it "parses object tags" do + expect(described_class.parse("O")).to be_a TJSON::DataType::Object + end + + it "parses arrays of objects" do + result = described_class.parse("A") + expect(result).to be_a TJSON::DataType::Array + expect(result.inner_type).to be_a TJSON::DataType::Object + end + end + + context "invalid tag" do + let(:invalid_tag) { "X" } + + it "raises TJSON::TypeError" do + expect { described_class.parse(invalid_tag) }.to raise_error(TJSON::TypeError) + end + end +end diff --git a/spec/tjson/generator_spec.rb b/spec/tjson/generator_spec.rb index eed192d..70be3b1 100644 --- a/spec/tjson/generator_spec.rb +++ b/spec/tjson/generator_spec.rb @@ -13,7 +13,7 @@ } end - it "round trips an example structure" do + xit "round trips an example structure" do tjson = TJSON.generate(example_structure) expect(TJSON.parse(tjson)).to eq example_structure end diff --git a/spec/tjson/object_spec.rb b/spec/tjson/object_spec.rb index 13aca03..c2d72a8 100644 --- a/spec/tjson/object_spec.rb +++ b/spec/tjson/object_spec.rb @@ -3,36 +3,8 @@ RSpec.describe TJSON::Object do subject(:object) { described_class.new } - describe "member name types" do - let(:example_value) { "s:bar".dup } - - context "Unicode Strings" do - let(:example_name) { "s:foo".dup } - - it "parses successfully" do - expect { object[example_name] = example_value }.not_to raise_error - end - end - - context "Binary Data" do - let(:example_name) { "b16:48656c6c6f2c20776f726c6421".dup } - - it "parses successfully" do - expect { object[example_name] = example_value }.not_to raise_error - end - end - - context "types other than Unicode Strings or Binary Data" do - let(:example_name) { "i:42".dup } - - it "raises TJSON::ParseError" do - expect { object[example_name] = example_value }.to raise_error(TJSON::ParseError) - end - end - end - describe "duplicate member names" do - let(:example_name) { "s:foo" } + let(:example_name) { "foobar:f" } it "raises TJSON::DuplicateNameError" do object[example_name.dup] = 1 diff --git a/spec/tjson/parser_spec.rb b/spec/tjson/parser_spec.rb deleted file mode 100644 index 30db3c5..0000000 --- a/spec/tjson/parser_spec.rb +++ /dev/null @@ -1,202 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe TJSON::Parser do - describe ".parse" do - context "UTF-8 strings" do - let(:example_result) { "hello, world!" } - let(:example_string) { "s:#{example_result}".dup } - - it "parses" do - expect(described_class.parse(example_string)).to eq example_result - end - end - - context "untagged strings" do - let(:example_string) { "hello, world!" } - - it "raises TJSON::ParseError" do - expect { described_class.parse(example_string) }.to raise_error(TJSON::ParseError) - end - end - end - - describe ".parse_base16" do - context "valid base16 string" do - let(:example_base16) { "48656c6c6f2c20776f726c6421" } - let(:example_result) { "Hello, world!" } - - it "parses successfully" do - result = described_class.parse_base16(example_base16) - expect(result).to eq example_result - expect(result.encoding).to eq Encoding::BINARY - end - end - - context "invalid base16 string" do - let(:invalid_base16) { "Surely this is not valid base16!" } - - it "raises TJSON::ParseError" do - expect { described_class.parse_base16(invalid_base16) }.to raise_error(TJSON::ParseError) - end - end - end - - describe ".parse_base32" do - context "valid base32 string" do - let(:example_base32) { "jbswy3dpfqqho33snrscc" } - let(:example_result) { "Hello, world!" } - - it "parses successfully" do - result = described_class.parse_base32(example_base32) - expect(result).to eq example_result - expect(result.encoding).to eq Encoding::BINARY - end - end - - context "padded base32 string" do - let(:padded_base32) { "jbswy3dpfqqho33snrscc===" } - - it "raises TJSON::ParseError" do - expect { described_class.parse_base32(padded_base32) }.to raise_error(TJSON::ParseError) - end - end - - context "invalid base32 string" do - let(:invalid_base32) { "Surely this is not valid base32!" } - - it "raises TJSON::ParseError" do - expect { described_class.parse_base32(invalid_base32) }.to raise_error(TJSON::ParseError) - end - end - end - - describe ".parse_base64url" do - context "valid base64url string" do - let(:example_base64url) { "SGVsbG8sIHdvcmxkIQ" } - let(:example_result) { "Hello, world!" } - - it "parses successfully" do - result = described_class.parse_base64url(example_base64url) - expect(result).to eq example_result - expect(result.encoding).to eq Encoding::BINARY - end - end - - context "padded base64url string" do - let(:padded_base64url) { "SGVsbG8sIHdvcmxkIQ==" } - - it "raises TJSON::ParseError" do - expect { described_class.parse_base16(padded_base64url) }.to raise_error(TJSON::ParseError) - end - end - - context "invalid base64url string" do - let(:invalid_base64url) { "Surely this is not valid base64url!" } - - it "raises TJSON::ParseError" do - expect { described_class.parse_base16(invalid_base64url) }.to raise_error(TJSON::ParseError) - end - end - end - - context "integers" do - describe ".parse_signed_int" do - context "valid integer string" do - let(:example_string) { "42" } - let(:example_integer) { 42 } - - it "parses successfully" do - expect(described_class.parse_signed_int(example_string)).to eq example_integer - end - end - - context "MAXINT for 64-bit signed integer" do - let(:example_string) { "9223372036854775807" } - let(:example_integer) { (2**63) - 1 } - - it "parses successfully" do - expect(described_class.parse_signed_int(example_string)).to eq example_integer - end - end - - context "-MAXINT for 64-bit signed integer" do - let(:example_string) { "-9223372036854775808" } - let(:example_integer) { -(2**63) } - - it "parses successfully" do - expect(described_class.parse_signed_int(example_string)).to eq example_integer - end - end - - context "oversized signed integer string" do - let(:oversized_example) { "9223372036854775808" } - - it "raises TJSON::ParseError" do - expect { described_class.parse_signed_int(oversized_example) }.to raise_error(TJSON::ParseError) - end - end - - context "undersized signed integer string" do - let(:oversized_example) { "-9223372036854775809" } - - it "raises TJSON::ParseError" do - expect { described_class.parse_signed_int(oversized_example) }.to raise_error(TJSON::ParseError) - end - end - end - - describe ".parse_unsigned_int" do - context "valid integer string" do - let(:example_string) { "42" } - let(:example_integer) { 42 } - - it "parses successfully" do - expect(described_class.parse_unsigned_int(example_string)).to eq example_integer - end - end - - context "MAXINT for 64-bit unsigned integer" do - let(:example_string) { "18446744073709551615" } - let(:example_integer) { (2**64) - 1 } - - it "parses successfully" do - expect(described_class.parse_unsigned_int(example_string)).to eq example_integer - end - end - - context "oversized unsigned integer string" do - let(:oversized_example) { "18446744073709551616" } - - it "raises TJSON::ParseError" do - expect { described_class.parse_unsigned_int(oversized_example) }.to raise_error(TJSON::ParseError) - end - end - - context "negative unsigned integer" do - let(:example_string) { "-1" } - - it "raises TJSON::ParseError" do - expect { described_class.parse_unsigned_int(example_string) }.to raise_error(TJSON::ParseError) - end - end - end - end - - describe ".parse_timestamp" do - context "valid UTC RFC3339 timestamp" do - let(:example_timestamp) { "2016-10-02T07:31:51Z" } - - it "parses successfully" do - expect(described_class.parse_timestamp(example_timestamp)).to be_a Time - end - end - - context "RFC3339 timestamp with non-UTC time zone" do - let(:invalid_timestamp) { "2016-10-02T07:31:51-08:00" } - - it "raises TJSON::ParseError" do - expect { described_class.parse_timestamp(invalid_timestamp) }.to raise_error(TJSON::ParseError) - end - end - end -end diff --git a/spec/tjson_spec.rb b/spec/tjson_spec.rb index b7b5f51..b7e6202 100644 --- a/spec/tjson_spec.rb +++ b/spec/tjson_spec.rb @@ -6,8 +6,6 @@ end describe ".parse" do - before { skip "rewrite for new syntax" } - context "draft-tjson-examples.txt" do ExampleLoader.new.examples.each do |example| if example.success? @@ -22,15 +20,6 @@ end end - # TODO: Remove when draft-tjson-examples has better coverage of object parsing - context "object placeholder" do - let(:example_data) { '{"s:hello": "s:world"}' } - - it "parses a simple TJSON object" do - expect { TJSON.parse(example_data) }.not_to raise_error - end - end - context "encoding" do let(:invalid_string) { "invalid\255".dup.force_encoding(Encoding::BINARY) }