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 +4 -4
- data/.rubocop.yml +3 -0
- data/CHANGES.md +4 -0
- data/Gemfile +4 -0
- data/Guardfile +17 -0
- data/README.md +5 -24
- data/lib/tjson.rb +11 -9
- data/lib/tjson/datatype.rb +108 -0
- data/lib/tjson/datatype/array.rb +37 -0
- data/lib/tjson/datatype/binary.rb +70 -0
- data/lib/tjson/datatype/float.rb +21 -0
- data/lib/tjson/datatype/integer.rb +48 -0
- data/lib/tjson/datatype/object.rb +33 -0
- data/lib/tjson/datatype/string.rb +22 -0
- data/lib/tjson/datatype/timestamp.rb +23 -0
- data/lib/tjson/object.rb +7 -8
- data/lib/tjson/version.rb +1 -1
- metadata +11 -6
- data/lib/tjson/array.rb +0 -10
- data/lib/tjson/binary.rb +0 -20
- data/lib/tjson/generator.rb +0 -49
- data/lib/tjson/parser.rb +0 -97
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a874be0629fb8b05f091435d8589d5d789a44865
|
4
|
+
data.tar.gz: 55d8f97a15fbcab3fd4022a8639d0d386e59e511
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e3146a94f973371a2d2091449282393eabffeed605279bd2c8876c27d6a4a430d4a986ffb1f417cf10ccdc9b5431916dccd5ad18a04bc232d1e38bff19132388
|
7
|
+
data.tar.gz: c7b0f974840341d988c36d0ddc0160f57850951ecc1d0055643968e90c360c9d2aeb675cd9373d0f88d3b2c2b44fc5ff4104a3ea85cb38221d37b68a07752740
|
data/.rubocop.yml
CHANGED
data/CHANGES.md
CHANGED
data/Gemfile
CHANGED
data/Guardfile
ADDED
@@ -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
|
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
|
-
* **
|
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 `
|
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
|
-
{"
|
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
|
data/lib/tjson.rb
CHANGED
@@ -7,11 +7,8 @@ require "time"
|
|
7
7
|
require "base32"
|
8
8
|
require "base64"
|
9
9
|
|
10
|
-
require "tjson/
|
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, "
|
66
|
-
JSON.generate(TJSON::
|
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
|
data/lib/tjson/object.rb
CHANGED
@@ -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 []=(
|
7
|
-
|
8
|
-
|
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
|
-
|
13
|
-
raise
|
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
|
-
|
13
|
+
type = TJSON::DataType.parse(result[:tag])
|
14
|
+
super(result[:name], type.convert(value))
|
16
15
|
end
|
17
16
|
end
|
18
17
|
end
|
data/lib/tjson/version.rb
CHANGED
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.
|
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-
|
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/
|
60
|
-
- lib/tjson/
|
61
|
-
- lib/tjson/
|
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
|
data/lib/tjson/array.rb
DELETED
data/lib/tjson/binary.rb
DELETED
@@ -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
|
data/lib/tjson/generator.rb
DELETED
@@ -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
|
data/lib/tjson/parser.rb
DELETED
@@ -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
|