tjson 0.0.0 → 0.1.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 +9 -0
- data/.travis.yml +2 -1
- data/CHANGES.md +3 -0
- data/Gemfile +2 -1
- data/LICENSE.txt +22 -0
- data/README.md +56 -2
- data/lib/tjson.rb +61 -0
- data/lib/tjson/array.rb +10 -0
- data/lib/tjson/binary.rb +20 -0
- data/lib/tjson/generator.rb +49 -0
- data/lib/tjson/object.rb +18 -0
- data/lib/tjson/parser.rb +97 -0
- data/lib/tjson/version.rb +1 -1
- data/tjson.gemspec +3 -0
- metadata +25 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 622fe5145c38438258607d59d9358ebe7cfe2d13
|
4
|
+
data.tar.gz: 7a534141296250d532964fbd7a1ccb3e67da126b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2f2ceb6fe96feea0f77e25ca5318c42aa2e744365aad316942e43904a842f3ca3938ae7fc16979cf2e03e973dfe752739ba83042f36e3eaeb1033da23712574e
|
7
|
+
data.tar.gz: f273f7abd7c79b80b819af9433e47589bb9e773062dae6132aa81138f1024a9cc4cbcd67f4a5b39de6258ab341798e84d63658a00a1465ca6a9df5750747b0d4
|
data/.rubocop.yml
CHANGED
data/.travis.yml
CHANGED
data/CHANGES.md
ADDED
data/Gemfile
CHANGED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2016 Tony Arcieri
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
CHANGED
@@ -1,9 +1,18 @@
|
|
1
|
-
# TJSON for Ruby
|
1
|
+
# TJSON for Ruby [![Gem Version][gem-image]][gem-link] [![Build Status][build-image]][build-link] [![Code Climate][codeclimate-image]][codeclimate-link] [![MIT licensed][license-image]][license-link]
|
2
2
|
|
3
3
|
A Ruby implementation of TJSON: Tagged JSON with Rich Types.
|
4
4
|
|
5
5
|
https://www.tjson.org
|
6
6
|
|
7
|
+
[gem-image]: https://badge.fury.io/rb/tjson.svg
|
8
|
+
[gem-link]: https://rubygems.org/gems/tjson
|
9
|
+
[build-image]: https://secure.travis-ci.org/tjson/tjson-ruby.svg?branch=master
|
10
|
+
[build-link]: https://travis-ci.org/tjson/tjson-ruby
|
11
|
+
[codeclimate-image]: https://codeclimate.com/github/tjson/tjson-ruby.svg?branch=master
|
12
|
+
[codeclimate-link]: https://codeclimate.com/github/tjson/tjson-ruby
|
13
|
+
[license-image]: https://img.shields.io/badge/license-MIT-blue.svg
|
14
|
+
[license-link]: https://github.com/tjson/tjson-ruby/blob/master/LICENSE.txt
|
15
|
+
|
7
16
|
## Installation
|
8
17
|
|
9
18
|
Add this line to your application's Gemfile:
|
@@ -22,7 +31,52 @@ Or install it yourself as:
|
|
22
31
|
|
23
32
|
## Usage
|
24
33
|
|
25
|
-
|
34
|
+
### Parsing
|
35
|
+
|
36
|
+
To parse a TJSON document, use the `TJSON.parse` method:
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
>> TJSON.parse('{"s:foo":"s:bar"}')
|
40
|
+
=> {"foo"=>"bar"}
|
41
|
+
```
|
42
|
+
|
43
|
+
The following describes how TJSON types map onto Ruby types during parsing:
|
44
|
+
|
45
|
+
* **UTF-8 Strings**: parsed as Ruby `String` with `Encoding::UTF_8`
|
46
|
+
* **Binary Data**: parsed as Ruby `String` with `Encoding::ASCII_8BIT` (a.k.a. `Encoding::BINARY`)
|
47
|
+
* **Integers**: parsed as Ruby `Integer` (Fixnum or Bignum)
|
48
|
+
* **Floats** (i.e. JSON number literals): parsed as Ruby `Float`
|
49
|
+
* **Timestamps**: parsed as Ruby `Time`
|
50
|
+
* **Arrays**: parsed as `TJSON::Array` (a subclass of `::Array`)
|
51
|
+
* **Objects**: parsed as `TJSON::Object` (a subclass of `::Hash`)
|
52
|
+
|
53
|
+
### Generating
|
54
|
+
|
55
|
+
To generate TJSON from Ruby objects, use the `TJSON.generate` method:
|
56
|
+
|
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"
|
79
|
+
```
|
26
80
|
|
27
81
|
## Contributing
|
28
82
|
|
data/lib/tjson.rb
CHANGED
@@ -2,6 +2,67 @@
|
|
2
2
|
|
3
3
|
require "tjson/version"
|
4
4
|
|
5
|
+
require "json"
|
6
|
+
require "time"
|
7
|
+
require "base32"
|
8
|
+
require "base64"
|
9
|
+
|
10
|
+
require "tjson/array"
|
11
|
+
require "tjson/binary"
|
12
|
+
require "tjson/generator"
|
13
|
+
require "tjson/object"
|
14
|
+
require "tjson/parser"
|
15
|
+
|
5
16
|
# Tagged JSON with Rich Types
|
6
17
|
module TJSON
|
18
|
+
# Base class of all TJSON errors
|
19
|
+
Error = Class.new(StandardError)
|
20
|
+
|
21
|
+
# Invalid string encoding
|
22
|
+
EncodingError = Class.new(Error)
|
23
|
+
|
24
|
+
# Failure to parse TJSON document
|
25
|
+
ParseError = Class.new(Error)
|
26
|
+
|
27
|
+
# Duplicate object name
|
28
|
+
DuplicateNameError = Class.new(ParseError)
|
29
|
+
|
30
|
+
# Maximum allowed nesting (TODO: use TJSON-specified maximum)
|
31
|
+
MAX_NESTING = 100
|
32
|
+
|
33
|
+
# Parse the given UTF-8 string as TJSON
|
34
|
+
#
|
35
|
+
# @param string [String] TJSON string to be parsed
|
36
|
+
# @raise [TJSON::ParseError] an error occurred parsing the given TJSON
|
37
|
+
# @return [Object] parsed data
|
38
|
+
def self.parse(string)
|
39
|
+
begin
|
40
|
+
utf8_string = string.encode(Encoding::UTF_8)
|
41
|
+
rescue ::EncodingError => ex
|
42
|
+
raise TJSON::EncodingError, ex.message, ex.backtrace
|
43
|
+
end
|
44
|
+
|
45
|
+
begin
|
46
|
+
::JSON.parse(
|
47
|
+
utf8_string,
|
48
|
+
max_nesting: MAX_NESTING,
|
49
|
+
allow_nan: false,
|
50
|
+
symbolize_names: false,
|
51
|
+
create_additions: false,
|
52
|
+
object_class: TJSON::Object,
|
53
|
+
array_class: TJSON::Array
|
54
|
+
)
|
55
|
+
rescue ::JSON::ParserError => ex
|
56
|
+
raise TJSON::ParseError, ex.message, ex.backtrace
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Generate TJSON from a Ruby Hash or Array
|
61
|
+
#
|
62
|
+
# @param obj [Array, Hash] Ruby Hash or Array to serialize as TJSON
|
63
|
+
# @return [String] serialized TJSON
|
64
|
+
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
|
+
end
|
7
68
|
end
|
data/lib/tjson/array.rb
ADDED
data/lib/tjson/binary.rb
ADDED
@@ -0,0 +1,20 @@
|
|
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
|
@@ -0,0 +1,49 @@
|
|
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/object.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TJSON
|
4
|
+
# TJSON object type (i.e. hash/dict-alike)
|
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
|
11
|
+
|
12
|
+
name = TJSON::Parser.value(name)
|
13
|
+
raise TJSON::DuplicateNameError, "duplicate member name: #{name.inspect}" if key?(name)
|
14
|
+
|
15
|
+
super(name, TJSON::Parser.value(value))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/tjson/parser.rb
ADDED
@@ -0,0 +1,97 @@
|
|
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
|
data/lib/tjson/version.rb
CHANGED
data/tjson.gemspec
CHANGED
@@ -9,6 +9,7 @@ Gem::Specification.new do |spec|
|
|
9
9
|
spec.version = TJSON::VERSION
|
10
10
|
spec.authors = ["Tony Arcieri"]
|
11
11
|
spec.email = ["bascule@gmail.com"]
|
12
|
+
spec.licenses = ["MIT"]
|
12
13
|
spec.homepage = "https://github.com/tjson/tjson-ruby"
|
13
14
|
spec.summary = "Tagged JSON with Rich Types"
|
14
15
|
spec.description = "A JSON-compatible serialization format with rich type annotations"
|
@@ -20,5 +21,7 @@ Gem::Specification.new do |spec|
|
|
20
21
|
|
21
22
|
spec.required_ruby_version = ">= 2.0"
|
22
23
|
|
24
|
+
spec.add_runtime_dependency "base32", ">= 0.3"
|
25
|
+
|
23
26
|
spec.add_development_dependency "bundler", "~> 1.13"
|
24
27
|
end
|
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tjson
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.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-
|
11
|
+
date: 2016-10-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: base32
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.3'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.3'
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: bundler
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -36,14 +50,22 @@ files:
|
|
36
50
|
- ".rubocop.yml"
|
37
51
|
- ".ruby-version"
|
38
52
|
- ".travis.yml"
|
53
|
+
- CHANGES.md
|
39
54
|
- Gemfile
|
55
|
+
- LICENSE.txt
|
40
56
|
- README.md
|
41
57
|
- Rakefile
|
42
58
|
- lib/tjson.rb
|
59
|
+
- lib/tjson/array.rb
|
60
|
+
- lib/tjson/binary.rb
|
61
|
+
- lib/tjson/generator.rb
|
62
|
+
- lib/tjson/object.rb
|
63
|
+
- lib/tjson/parser.rb
|
43
64
|
- lib/tjson/version.rb
|
44
65
|
- tjson.gemspec
|
45
66
|
homepage: https://github.com/tjson/tjson-ruby
|
46
|
-
licenses:
|
67
|
+
licenses:
|
68
|
+
- MIT
|
47
69
|
metadata: {}
|
48
70
|
post_install_message:
|
49
71
|
rdoc_options: []
|