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 +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
|