tjson 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 622fe5145c38438258607d59d9358ebe7cfe2d13
4
- data.tar.gz: 7a534141296250d532964fbd7a1ccb3e67da126b
3
+ metadata.gz: a874be0629fb8b05f091435d8589d5d789a44865
4
+ data.tar.gz: 55d8f97a15fbcab3fd4022a8639d0d386e59e511
5
5
  SHA512:
6
- metadata.gz: 2f2ceb6fe96feea0f77e25ca5318c42aa2e744365aad316942e43904a842f3ca3938ae7fc16979cf2e03e973dfe752739ba83042f36e3eaeb1033da23712574e
7
- data.tar.gz: f273f7abd7c79b80b819af9433e47589bb9e773062dae6132aa81138f1024a9cc4cbcd67f4a5b39de6258ab341798e84d63658a00a1465ca6a9df5750747b0d4
6
+ metadata.gz: e3146a94f973371a2d2091449282393eabffeed605279bd2c8876c27d6a4a430d4a986ffb1f417cf10ccdc9b5431916dccd5ad18a04bc232d1e38bff19132388
7
+ data.tar.gz: c7b0f974840341d988c36d0ddc0160f57850951ecc1d0055643968e90c360c9d2aeb675cd9373d0f88d3b2c2b44fc5ff4104a3ea85cb38221d37b68a07752740
@@ -21,6 +21,9 @@ Metrics/AbcSize:
21
21
  Metrics/CyclomaticComplexity:
22
22
  Enabled: false
23
23
 
24
+ Metrics/PerceivedComplexity:
25
+ Enabled: false
26
+
24
27
  Metrics/ClassLength:
25
28
  Max: 100
26
29
 
data/CHANGES.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 0.2.0 (2016-11-06)
2
+
3
+ * Complete rewrite to support new postfix syntax
4
+
1
5
  ## 0.1.0 (2016-10-28)
2
6
 
3
7
  * Initial release
data/Gemfile CHANGED
@@ -4,6 +4,10 @@ source "https://rubygems.org"
4
4
 
5
5
  gemspec
6
6
 
7
+ group :development do
8
+ gem "guard-rspec"
9
+ end
10
+
7
11
  group :development, :test do
8
12
  gem "rake"
9
13
  gem "toml-rb"
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard :rspec, cmd: "GUARD_RSPEC=1 bundle exec rspec --no-profile" do
5
+ require "guard/rspec/dsl"
6
+ dsl = Guard::RSpec::Dsl.new(self)
7
+
8
+ # RSpec files
9
+ rspec = dsl.rspec
10
+ watch(rspec.spec_helper) { rspec.spec_dir }
11
+ watch(rspec.spec_support) { rspec.spec_dir }
12
+ watch(rspec.spec_files)
13
+
14
+ # Ruby files
15
+ ruby = dsl.ruby
16
+ dsl.watch_spec_files_for(ruby.lib_files)
17
+ end
data/README.md CHANGED
@@ -36,18 +36,18 @@ Or install it yourself as:
36
36
  To parse a TJSON document, use the `TJSON.parse` method:
37
37
 
38
38
  ```ruby
39
- >> TJSON.parse('{"s:foo":"s:bar"}')
39
+ >> TJSON.parse('{"foo:s":"bar"}')
40
40
  => {"foo"=>"bar"}
41
41
  ```
42
42
 
43
43
  The following describes how TJSON types map onto Ruby types during parsing:
44
44
 
45
- * **UTF-8 Strings**: parsed as Ruby `String` with `Encoding::UTF_8`
45
+ * **Unicode Strings**: parsed as Ruby `String` with `Encoding::UTF_8`
46
46
  * **Binary Data**: parsed as Ruby `String` with `Encoding::ASCII_8BIT` (a.k.a. `Encoding::BINARY`)
47
47
  * **Integers**: parsed as Ruby `Integer` (Fixnum or Bignum)
48
48
  * **Floats** (i.e. JSON number literals): parsed as Ruby `Float`
49
49
  * **Timestamps**: parsed as Ruby `Time`
50
- * **Arrays**: parsed as `TJSON::Array` (a subclass of `::Array`)
50
+ * **Arrays**: parsed as Ruby `Array`
51
51
  * **Objects**: parsed as `TJSON::Object` (a subclass of `::Hash`)
52
52
 
53
53
  ### Generating
@@ -55,27 +55,8 @@ The following describes how TJSON types map onto Ruby types during parsing:
55
55
  To generate TJSON from Ruby objects, use the `TJSON.generate` method:
56
56
 
57
57
  ```ruby
58
- >> puts TJSON.generate({"foo" => "bar" })
59
- {"s:foo":"s:bar"}
60
- ```
61
-
62
- The `TJSON.generate` method will call `#to_tjson` on any objects which are not
63
- one of Ruby's core types. You can implement this method on classes you wish to
64
- serialize as TJSON, although it MUST output tagged strings for encoding TJSON
65
- types such as UTF-8 strings or binary data.
66
-
67
- ### TJSON::Binary
68
-
69
- The `TJSON::Binary` module contains a set of helper methods for serializing
70
- binary data in various different encodings:
71
-
72
- ```ruby
73
- >> TJSON::Binary.base16("Hello, world!")
74
- => "b16:48656c6c6f2c20776f726c6421"
75
- >> TJSON::Binary.base32("Hello, world!")
76
- => "b32:jbswy3dpfqqho33snrscc"
77
- >> TJSON::Binary.base64("Hello, world!")
78
- => "b64:SGVsbG8sIHdvcmxkIQ"
58
+ >> puts TJSON.generate({"foo" => "bar"})
59
+ {"foo:s:"bar"}
79
60
  ```
80
61
 
81
62
  ## Contributing
@@ -7,11 +7,8 @@ require "time"
7
7
  require "base32"
8
8
  require "base64"
9
9
 
10
- require "tjson/array"
11
- require "tjson/binary"
12
- require "tjson/generator"
10
+ require "tjson/datatype"
13
11
  require "tjson/object"
14
- require "tjson/parser"
15
12
 
16
13
  # Tagged JSON with Rich Types
17
14
  module TJSON
@@ -24,6 +21,9 @@ module TJSON
24
21
  # Failure to parse TJSON document
25
22
  ParseError = Class.new(Error)
26
23
 
24
+ # Invalid types
25
+ TypeError = Class.new(ParseError)
26
+
27
27
  # Duplicate object name
28
28
  DuplicateNameError = Class.new(ParseError)
29
29
 
@@ -43,18 +43,20 @@ module TJSON
43
43
  end
44
44
 
45
45
  begin
46
- ::JSON.parse(
46
+ object = ::JSON.parse(
47
47
  utf8_string,
48
48
  max_nesting: MAX_NESTING,
49
49
  allow_nan: false,
50
50
  symbolize_names: false,
51
51
  create_additions: false,
52
- object_class: TJSON::Object,
53
- array_class: TJSON::Array
52
+ object_class: TJSON::Object
54
53
  )
55
54
  rescue ::JSON::ParserError => ex
56
55
  raise TJSON::ParseError, ex.message, ex.backtrace
57
56
  end
57
+
58
+ raise TJSON::TypeError, "invalid toplevel type: #{object.class}" unless object.is_a?(TJSON::Object)
59
+ object
58
60
  end
59
61
 
60
62
  # Generate TJSON from a Ruby Hash or Array
@@ -62,7 +64,7 @@ module TJSON
62
64
  # @param obj [Array, Hash] Ruby Hash or Array to serialize as TJSON
63
65
  # @return [String] serialized TJSON
64
66
  def self.generate(obj)
65
- raise TypeError, "expected Hash or Array, got #{obj.class}" unless obj.is_a?(Hash) || obj.is_a?(Array)
66
- JSON.generate(TJSON::Generator.generate(obj))
67
+ raise TypeError, "toplevel type must be a Hash" unless obj.is_a?(Hash)
68
+ JSON.generate(TJSON::DataType.generate(obj))
67
69
  end
68
70
  end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TJSON
4
+ # Hierarchy of TJSON types
5
+ class DataType
6
+ # Find a type by its tag
7
+ def self.[](tag)
8
+ TAGS[tag] || raise(TJSON::TypeError, "unknown tag: #{tag.inspect}")
9
+ end
10
+
11
+ def self.parse(tag)
12
+ raise TJSON::TypeError, "expected String, got #{tag.class}" unless tag.is_a?(::String)
13
+
14
+ if tag == "O"
15
+ # Object
16
+ TJSON::DataType[tag]
17
+ elsif (result = tag.match(/\A(?<type>[A-Z][a-z0-9]*)\<(?<inner>.*)\>\z/))
18
+ # Non-scalar
19
+ inner = parse(result[:inner]) unless result[:inner].empty?
20
+ TJSON::DataType[result[:type]].new(inner).freeze
21
+ elsif tag =~ /\A[a-z][a-z0-9]*\z/
22
+ # Scalar
23
+ TJSON::DataType[tag]
24
+ else
25
+ raise TJSON::ParseError, "couldn't parse tag: #{tag.inspect}" unless result
26
+ end
27
+ end
28
+
29
+ def self.identify_type(obj)
30
+ case obj
31
+ when Hash then self["O"]
32
+ when ::Array then TJSON::DataType::Array.identify_type(obj)
33
+ when ::String, Symbol then obj.encoding == Encoding::BINARY ? self["b"] : self["s"]
34
+ when ::Integer then self["i"]
35
+ when ::Float then self["f"]
36
+ when ::Time, ::DateTime then self["t"]
37
+ else raise TypeError, "don't know how to serialize #{obj.class} as TJSON"
38
+ end
39
+ end
40
+
41
+ def self.generate(obj)
42
+ identify_type(obj).generate(obj)
43
+ end
44
+
45
+ # Scalar types
46
+ class Scalar < TJSON::DataType
47
+ def scalar?
48
+ true
49
+ end
50
+
51
+ def inspect
52
+ "#<#{self.class}>"
53
+ end
54
+ end
55
+
56
+ # Non-scalar types
57
+ class NonScalar < TJSON::DataType
58
+ attr_reader :inner_type
59
+
60
+ def initialize(inner_type)
61
+ @inner_type = inner_type
62
+ end
63
+
64
+ def inspect
65
+ "#<#{self.class}<#{@inner_type.inspect}>>"
66
+ end
67
+
68
+ def scalar?
69
+ false
70
+ end
71
+
72
+ def ==(other)
73
+ self.class == other.class && inner_type == other.inner_type
74
+ end
75
+ end
76
+
77
+ # Numbers
78
+ class Number < Scalar; end
79
+ end
80
+ end
81
+
82
+ require "tjson/datatype/array"
83
+ require "tjson/datatype/binary"
84
+ require "tjson/datatype/float"
85
+ require "tjson/datatype/integer"
86
+ require "tjson/datatype/string"
87
+ require "tjson/datatype/timestamp"
88
+ require "tjson/datatype/object"
89
+
90
+ # TJSON does not presently support user-extensible types
91
+ TJSON::DataType::TAGS = {
92
+ # Object (non-scalar with self-describing types)
93
+ "O" => TJSON::DataType::Object.new(nil).freeze,
94
+
95
+ # Non-scalars
96
+ "A" => TJSON::DataType::Array,
97
+
98
+ # Scalars
99
+ "b" => TJSON::DataType::Binary64.new.freeze,
100
+ "b16" => TJSON::DataType::Binary16.new.freeze,
101
+ "b32" => TJSON::DataType::Binary32.new.freeze,
102
+ "b64" => TJSON::DataType::Binary64.new.freeze,
103
+ "f" => TJSON::DataType::Float.new.freeze,
104
+ "i" => TJSON::DataType::SignedInt.new.freeze,
105
+ "s" => TJSON::DataType::String.new.freeze,
106
+ "t" => TJSON::DataType::Timestamp.new.freeze,
107
+ "u" => TJSON::DataType::UnsignedInt.new.freeze
108
+ }.freeze
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TJSON
4
+ class DataType
5
+ # TJSON arrays
6
+ class Array < NonScalar
7
+ # Determine the type of a Ruby array (for serialization)
8
+ def self.identify_type(array)
9
+ inner_type = nil
10
+
11
+ array.each do |elem|
12
+ t = TJSON::DataType.identify_type(elem)
13
+ inner_type ||= t
14
+ raise TJSON::TypeError, "array contains heterogenous types: #{array.inspect}" unless inner_type == t
15
+ end
16
+
17
+ new(inner_type)
18
+ end
19
+
20
+ def tag
21
+ "A<#{@inner_type.tag}>"
22
+ end
23
+
24
+ def convert(array)
25
+ raise TJSON::TypeError, "expected Array, got #{array.class}" unless array.is_a?(::Array)
26
+
27
+ return array.map! { |o| @inner_type.convert(o) } if @inner_type
28
+ return array if array.empty?
29
+ raise TJSON::ParseError, "no inner type specified for non-empty array: #{array.inspect}"
30
+ end
31
+
32
+ def generate(array)
33
+ array.map { |o| TJSON::DataType.generate(o) }
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TJSON
4
+ class DataType
5
+ # Binary Data
6
+ class Binary < Scalar; end
7
+
8
+ # Base16-serialized binary data
9
+ class Binary16 < Binary
10
+ def tag
11
+ "b16"
12
+ end
13
+
14
+ def convert(str)
15
+ raise TJSON::TypeError, "expected String, got #{str.class}: #{str.inspect}" unless str.is_a?(::String)
16
+ raise TJSON::ParseError, "base16 must be lower case: #{str.inspect}" if str =~ /[A-F]/
17
+ raise TJSON::ParseError, "invalid base16: #{str.inspect}" unless str =~ /\A[a-f0-9]*\z/
18
+
19
+ [str].pack("H*")
20
+ end
21
+
22
+ def generate(binary)
23
+ binary.unpack("H*").first
24
+ end
25
+ end
26
+
27
+ # Base32-serialized binary data
28
+ class Binary32 < Binary
29
+ def tag
30
+ "b32"
31
+ end
32
+
33
+ def convert(str)
34
+ raise TJSON::TypeError, "expected String, got #{str.class}: #{str.inspect}" unless str.is_a?(::String)
35
+ raise TJSON::ParseError, "base32 must be lower case: #{str.inspect}" if str =~ /[A-Z]/
36
+ raise TJSON::ParseError, "padding disallowed: #{str.inspect}" if str.include?("=")
37
+ raise TJSON::ParseError, "invalid base32: #{str.inspect}" unless str =~ /\A[a-z2-7]*\z/
38
+
39
+ ::Base32.decode(str.upcase).force_encoding(Encoding::BINARY)
40
+ end
41
+
42
+ def generate(binary)
43
+ Base32.encode(binary).downcase.delete("=")
44
+ end
45
+ end
46
+
47
+ # Base64-serialized binary data
48
+ class Binary64 < Binary
49
+ def tag
50
+ "b64"
51
+ end
52
+
53
+ def convert(str)
54
+ raise TJSON::TypeError, "expected String, got #{str.class}: #{str.inspect}" unless str.is_a?(::String)
55
+ raise TJSON::ParseError, "base64url only: #{str.inspect}" if str =~ %r{\+|\/}
56
+ raise TJSON::ParseError, "padding disallowed: #{str.inspect}" if str.include?("=")
57
+ raise TJSON::ParseError, "invalid base64url: #{str.inspect}" unless str =~ /\A[A-Za-z0-9\-_]*\z/
58
+
59
+ # Add padding, as older Rubies (< 2.3) require it
60
+ str = str.ljust((str.length + 3) & ~3, "=") if (str.length % 4).nonzero?
61
+
62
+ ::Base64.urlsafe_decode64(str)
63
+ end
64
+
65
+ def generate(binary)
66
+ Base64.urlsafe_encode64(binary).delete("=")
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TJSON
4
+ class DataType
5
+ # Floating point type
6
+ class Float < Number
7
+ def tag
8
+ "f"
9
+ end
10
+
11
+ def convert(float)
12
+ raise TJSON::TypeError, "expected Float, got #{float.class}" unless float.is_a?(::Numeric)
13
+ float.to_f
14
+ end
15
+
16
+ def generate(float)
17
+ float.to_f
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TJSON
4
+ class DataType
5
+ # Base class of integer types
6
+ class Integer < Scalar
7
+ def generate(int)
8
+ # Integers are serialized as strings to sidestep the limits of some JSON parsers
9
+ int.to_s
10
+ end
11
+ end
12
+
13
+ # Signed 64-bit integer
14
+ class SignedInt < Integer
15
+ def tag
16
+ "i"
17
+ end
18
+
19
+ def convert(str)
20
+ raise TJSON::TypeError, "expected String, got #{str.class}: #{str.inspect}" unless str.is_a?(::String)
21
+ raise TJSON::ParseError, "invalid integer: #{str.inspect}" unless str =~ /\A\-?(0|[1-9][0-9]*)\z/
22
+
23
+ result = Integer(str, 10)
24
+ raise TJSON::ParseError, "oversized integer: #{result}" if result > 9_223_372_036_854_775_807
25
+ raise TJSON::ParseError, "undersized integer: #{result}" if result < -9_223_372_036_854_775_808
26
+
27
+ result
28
+ end
29
+ end
30
+
31
+ # Unsigned 64-bit integer
32
+ class UnsignedInt < Integer
33
+ def tag
34
+ "u"
35
+ end
36
+
37
+ def convert(str)
38
+ raise TJSON::TypeError, "expected String, got #{str.class}: #{str.inspect}" unless str.is_a?(::String)
39
+ raise TJSON::ParseError, "invalid integer: #{str.inspect}" unless str =~ /\A(0|[1-9][0-9]*)\z/
40
+
41
+ result = Integer(str, 10)
42
+ raise TJSON::ParseError, "oversized integer: #{result}" if result > 18_446_744_073_709_551_615
43
+
44
+ result
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TJSON
4
+ class DataType
5
+ # TJSON objects
6
+ class Object < NonScalar
7
+ def tag
8
+ "O"
9
+ end
10
+
11
+ def convert(obj)
12
+ raise TJSON::TypeError, "expected TJSON::Object, got #{obj.class}" unless obj.is_a?(TJSON::Object)
13
+
14
+ # Objects handle their own member conversions
15
+ obj
16
+ end
17
+
18
+ def generate(obj)
19
+ members = obj.map do |k, v|
20
+ raise TypeError, "expected String for key, got #{k.class}" unless k.is_a?(::String)
21
+ type = TJSON::DataType.identify_type(v)
22
+ ["#{k}:#{type.tag}", TJSON::DataType.generate(v)]
23
+ end
24
+
25
+ Hash[members]
26
+ end
27
+
28
+ def inspect
29
+ "#<#{self.class}>"
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TJSON
4
+ class DataType
5
+ # Unicode String type
6
+ class String < Scalar
7
+ def tag
8
+ "s"
9
+ end
10
+
11
+ def convert(str)
12
+ raise TJSON::TypeError, "expected String, got #{str.class}: #{str.inspect}" unless str.is_a?(::String)
13
+ raise TJSON::EncodingError, "expected UTF-8, got #{str.encoding.inspect}" unless str.encoding == Encoding::UTF_8
14
+ str
15
+ end
16
+
17
+ def generate(obj)
18
+ obj.to_s
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TJSON
4
+ class DataType
5
+ # RFC3339 timestamp (Z-normalized)
6
+ class Timestamp < Scalar
7
+ def tag
8
+ "t"
9
+ end
10
+
11
+ def convert(str)
12
+ raise TJSON::TypeError, "expected String, got #{str.class}: #{str.inspect}" unless str.is_a?(::String)
13
+ 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/
14
+
15
+ ::Time.iso8601(str)
16
+ end
17
+
18
+ def generate(timestamp)
19
+ timestamp.to_time.utc.iso8601
20
+ end
21
+ end
22
+ end
23
+ end
@@ -3,16 +3,15 @@
3
3
  module TJSON
4
4
  # TJSON object type (i.e. hash/dict-alike)
5
5
  class Object < ::Hash
6
- def []=(name, value)
7
- unless name.start_with?("s:", "b16:", "b64:")
8
- raise TJSON::ParseError, "invalid tag on object name: #{name[/\A(.*?):/, 1]}" if name.include?(":")
9
- raise TJSON::ParseError, "no tag found on object name: #{name.inspect}"
10
- end
6
+ def []=(tagged_name, value)
7
+ # NOTE: this regex is sloppy. The real parsing is performed in TJSON::DataType#parse
8
+ result = tagged_name.match(/\A(?<name>.*):(?<tag>[A-Za-z0-9\<]+[\>]*)\z/)
11
9
 
12
- name = TJSON::Parser.value(name)
13
- raise TJSON::DuplicateNameError, "duplicate member name: #{name.inspect}" if key?(name)
10
+ raise ParseError, "invalid tag: #{tagged_name.inspect}" unless result
11
+ raise DuplicateNameError, "duplicate member name: #{result[:name].inspect}" if key?(result[:name])
14
12
 
15
- super(name, TJSON::Parser.value(value))
13
+ type = TJSON::DataType.parse(result[:tag])
14
+ super(result[:name], type.convert(value))
16
15
  end
17
16
  end
18
17
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TJSON
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tjson
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tony Arcieri
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-10-28 00:00:00.000000000 Z
11
+ date: 2016-11-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base32
@@ -52,15 +52,20 @@ files:
52
52
  - ".travis.yml"
53
53
  - CHANGES.md
54
54
  - Gemfile
55
+ - Guardfile
55
56
  - LICENSE.txt
56
57
  - README.md
57
58
  - Rakefile
58
59
  - lib/tjson.rb
59
- - lib/tjson/array.rb
60
- - lib/tjson/binary.rb
61
- - lib/tjson/generator.rb
60
+ - lib/tjson/datatype.rb
61
+ - lib/tjson/datatype/array.rb
62
+ - lib/tjson/datatype/binary.rb
63
+ - lib/tjson/datatype/float.rb
64
+ - lib/tjson/datatype/integer.rb
65
+ - lib/tjson/datatype/object.rb
66
+ - lib/tjson/datatype/string.rb
67
+ - lib/tjson/datatype/timestamp.rb
62
68
  - lib/tjson/object.rb
63
- - lib/tjson/parser.rb
64
69
  - lib/tjson/version.rb
65
70
  - tjson.gemspec
66
71
  homepage: https://github.com/tjson/tjson-ruby
@@ -1,10 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module TJSON
4
- # TJSON array type
5
- class Array < ::Array
6
- def <<(obj)
7
- super(TJSON::Parser.value(obj))
8
- end
9
- end
10
- end
@@ -1,20 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module TJSON
4
- # Binary serialization helpers
5
- module Binary
6
- module_function
7
-
8
- def base16(string)
9
- "b16:#{string.unpack('H*').first}"
10
- end
11
-
12
- def base32(string)
13
- "b32:#{Base32.encode(string).downcase.delete('=')}"
14
- end
15
-
16
- def base64(string)
17
- "b64:#{Base64.urlsafe_encode64(string).delete('=')}"
18
- end
19
- end
20
- end
@@ -1,49 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module TJSON
4
- # Generates TJSON from Ruby objects
5
- module Generator
6
- module_function
7
-
8
- def generate(obj)
9
- case obj
10
- when Hash then generate_hash(obj)
11
- when ::Array then generate_array(obj)
12
- when String, Symbol then generate_string(obj.to_s)
13
- when Integer then generate_integer(obj)
14
- when Float then obj
15
- when Time, DateTime then generate_timestamp(obj.to_time)
16
- else obj.to_tjson
17
- end
18
- end
19
-
20
- def generate_hash(hash)
21
- members = hash.map do |k, v|
22
- raise TypeError, "expected String for key, got #{k.class}" unless k.is_a?(String)
23
- [generate(k), generate(v)]
24
- end
25
-
26
- Hash[members]
27
- end
28
-
29
- def generate_array(array)
30
- array.map { |o| generate(o) }
31
- end
32
-
33
- def generate_string(string)
34
- if string.encoding == Encoding::BINARY
35
- TJSON::Binary.base64(string)
36
- else
37
- "s:#{string.encode(Encoding::UTF_8)}"
38
- end
39
- end
40
-
41
- def generate_integer(int)
42
- "i:#{int}"
43
- end
44
-
45
- def generate_timestamp(time)
46
- "t:#{time.utc.iso8601}"
47
- end
48
- end
49
- end
@@ -1,97 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module TJSON
4
- # Postprocessing for extracting TJSON tags from JSON
5
- module Parser
6
- module_function
7
-
8
- TAG_DELIMITER = ":"
9
- MAX_TAG_LENGTH = 3 # Sans ':'
10
-
11
- def value(obj)
12
- case obj
13
- when String then parse(obj)
14
- when Integer then obj.to_f
15
- when TJSON::Object, TJSON::Array, Float then obj
16
- else raise TypeError, "invalid TJSON value: #{obj.inspect}"
17
- end
18
- end
19
-
20
- def parse(str)
21
- raise TypeError, "expected String, got #{str.class}" unless str.is_a?(::String)
22
- raise TJSON::EncodingError, "expected UTF-8, got #{str.encoding.inspect}" unless str.encoding == Encoding::UTF_8
23
-
24
- dpos = str.index(TAG_DELIMITER)
25
-
26
- raise TJSON::ParseError, "invalid tag (missing ':' delimiter)" unless dpos
27
- raise TJSON::ParseError, "overlength tag (maximum #{MAX_TAG_LENGTH})" if dpos > MAX_TAG_LENGTH
28
-
29
- tag = str.slice!(0, dpos + 1)
30
-
31
- case tag
32
- when "s:" then str
33
- when "i:" then parse_signed_int(str)
34
- when "u:" then parse_unsigned_int(str)
35
- when "t:" then parse_timestamp(str)
36
- when "b16:" then parse_base16(str)
37
- when "b32:" then parse_base32(str)
38
- when "b64:" then parse_base64url(str)
39
- else raise TJSON::ParseError, "invalid tag #{tag.inspect} on string #{str.inspect}"
40
- end
41
- end
42
-
43
- def parse_base16(str)
44
- raise TypeError, "expected String, got #{str.class}" unless str.is_a?(::String)
45
- raise TJSON::ParseError, "base16 must be lower case: #{str.inspect}" if str =~ /[A-F]/
46
- raise TJSON::ParseError, "invalid base16: #{str.inspect}" unless str =~ /\A[a-f0-9]*\z/
47
-
48
- [str].pack("H*")
49
- end
50
-
51
- def parse_base32(str)
52
- raise TypeError, "expected String, got #{str.class}" unless str.is_a?(::String)
53
- raise TJSON::ParseError, "base32 must be lower case: #{str.inspect}" if str =~ /[A-F]/
54
- raise TJSON::ParseError, "padding disallowed: #{str.inspect}" if str.include?("=")
55
- raise TJSON::ParseError, "invalid base32: #{str.inspect}" unless str =~ /\A[a-z2-7]*\z/
56
-
57
- Base32.decode(str.upcase).force_encoding(Encoding::BINARY)
58
- end
59
-
60
- def parse_base64url(str)
61
- raise TypeError, "expected String, got #{str.class}" unless str.is_a?(::String)
62
- raise TJSON::ParseError, "base64url only: #{str.inspect}" if str =~ %r{\+|\/}
63
- raise TJSON::ParseError, "padding disallowed: #{str.inspect}" if str.include?("=")
64
- raise TJSON::ParseError, "invalid base64url: #{str.inspect}" unless str =~ /\A[A-Za-z0-9\-_]*\z/
65
-
66
- # Add padding, as older Rubies (< 2.3) require it
67
- str = str.ljust((str.length + 3) & ~3, "=") if (str.length % 4).nonzero?
68
-
69
- Base64.urlsafe_decode64(str)
70
- end
71
-
72
- def parse_signed_int(str)
73
- raise TJSON::ParseError, "invalid integer: #{str.inspect}" unless str =~ /\A\-?(0|[1-9][0-9]*)\z/
74
-
75
- result = Integer(str, 10)
76
- raise TJSON::ParseError, "oversized integer: #{result}" if result > 9_223_372_036_854_775_807
77
- raise TJSON::ParseError, "undersized integer: #{result}" if result < -9_223_372_036_854_775_808
78
-
79
- result
80
- end
81
-
82
- def parse_unsigned_int(str)
83
- raise TJSON::ParseError, "invalid integer: #{str.inspect}" unless str =~ /\A(0|[1-9][0-9]*)\z/
84
-
85
- result = Integer(str, 10)
86
- raise TJSON::ParseError, "oversized integer: #{result}" if result > 18_446_744_073_709_551_615
87
-
88
- result
89
- end
90
-
91
- def parse_timestamp(str)
92
- 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/
93
-
94
- Time.iso8601(str)
95
- end
96
- end
97
- end