tjson 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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