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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d7d2463b75101ea02b80b1b44ebc77dec455378b
4
- data.tar.gz: 6c2ea2c56a7f17de9de38ba66f32b270fd1e2840
3
+ metadata.gz: 622fe5145c38438258607d59d9358ebe7cfe2d13
4
+ data.tar.gz: 7a534141296250d532964fbd7a1ccb3e67da126b
5
5
  SHA512:
6
- metadata.gz: caff166f158c153e930023b91477921298bbe24bf840f4de50c63bd350c274d3216667e28125417c3f61b66108ba408f78afb14c18fefbb3d0cec70342214d71
7
- data.tar.gz: ce78fb8dea9c903a3cecbd1947e8d3c83f4ad7904b62c1c72306ba25ec7232fd36f99fd98afb97d542d20a0e1cc98531b8170000c87780a7ebc0974a5afd2f52
6
+ metadata.gz: 2f2ceb6fe96feea0f77e25ca5318c42aa2e744365aad316942e43904a842f3ca3938ae7fc16979cf2e03e973dfe752739ba83042f36e3eaeb1033da23712574e
7
+ data.tar.gz: f273f7abd7c79b80b819af9433e47589bb9e773062dae6132aa81138f1024a9cc4cbcd67f4a5b39de6258ab341798e84d63658a00a1465ca6a9df5750747b0d4
@@ -15,5 +15,14 @@ Style/StringLiterals:
15
15
  # Metrics
16
16
  #
17
17
 
18
+ Metrics/AbcSize:
19
+ Enabled: false
20
+
21
+ Metrics/CyclomaticComplexity:
22
+ Enabled: false
23
+
18
24
  Metrics/ClassLength:
19
25
  Max: 100
26
+
27
+ Metrics/MethodLength:
28
+ Max: 25
@@ -1,5 +1,6 @@
1
1
  language: ruby
2
2
  sudo: false
3
+ before_install: gem install bundler
3
4
 
4
5
  bundler_args: --without development doc
5
6
 
@@ -8,7 +9,7 @@ rvm:
8
9
  - 2.1
9
10
  - 2.2
10
11
  - 2.3.1
11
- - jruby-9.0.5.0
12
+ - jruby-9.1.5.0
12
13
 
13
14
  matrix:
14
15
  fast_finish: true
@@ -0,0 +1,3 @@
1
+ ## 0.1.0 (2016-10-28)
2
+
3
+ * Initial release
data/Gemfile CHANGED
@@ -6,6 +6,7 @@ gemspec
6
6
 
7
7
  group :development, :test do
8
8
  gem "rake"
9
+ gem "toml-rb"
9
10
  gem "rspec", "~> 3.5"
10
- gem "rubocop"
11
+ gem "rubocop", "0.44.1"
11
12
  end
@@ -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
- Coming soon!
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
 
@@ -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
@@ -0,0 +1,10 @@
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
@@ -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
@@ -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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TJSON
4
- VERSION = "0.0.0"
4
+ VERSION = "0.1.0"
5
5
  end
@@ -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.0.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-02 00:00:00.000000000 Z
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: []