tjson 0.0.0 → 0.1.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 +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: []
|