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 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: []