tinycode 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.
data/README.md ADDED
@@ -0,0 +1,159 @@
1
+ # Tinycode
2
+
3
+ This gem provides an encoder and decoder for tinycode, which is a bencode like
4
+ encoding that supports more data-types and slightly smaller encoded sizes.
5
+
6
+ The encoder supports arrays, hashes, strings, symbols, and integers, as well as
7
+ specialized fixed-width integers: `Tinycode::U8`, `Tinycode::U16`,
8
+ `Tinycode::U32`, and `Tinycode::U64`.
9
+ Like [YAML] but unlike [JSON], this encoder maintains a distinction between
10
+ strings and symbols.
11
+
12
+ The encoding is inspired by [BEncode]'s tag-length-value construction for strings, which eliminates the need for escaping in Strings.
13
+ This makes this encoding particularly appropriate for serializing binary strings
14
+ directly.
15
+
16
+ [YAML]: https://ruby-doc.org/stdlib-3.0.2/libdoc/yaml/rdoc/YAML.html
17
+ [JSON]: https://ruby-doc.org/stdlib-3.0.2/libdoc/json/rdoc/JSON.html
18
+ [BEncode]: https://en.wikipedia.org/wiki/Bencode
19
+
20
+ ## Demo
21
+
22
+ Consider the following data structure:
23
+
24
+ ```ruby
25
+ [
26
+ {
27
+ location: 'Chicago',
28
+ currently: {
29
+ temperature: 72,
30
+ condition: ['Partly cloudy', 'Chance of rain']
31
+ },
32
+ narrative: "High: 81, low: 56\nCooler than yesterday.",
33
+ high: 81,
34
+ low: 56
35
+ },
36
+ {
37
+ location: 'New York',
38
+ currently: {
39
+ temperature: 45,
40
+ condition: ['Rainy']
41
+ },
42
+ narrative: "High: 56, low: -2\nWinter Weather Alert in effect.",
43
+ high: 56,
44
+ low: -2
45
+ }
46
+ ]
47
+ ```
48
+
49
+ The resulting structure looks something like this. In actuality, there is not
50
+ this much whitespace, and the \xNN codes represent binary values.
51
+
52
+ ```plain
53
+ [
54
+ {
55
+ currently:{
56
+ condition:[
57
+ Partly cloudy'
58
+ Chance of rain'
59
+ ]
60
+ temperature:+\x48
61
+ }
62
+ high:+\x51
63
+ location:Chicago'
64
+ low:+\x38
65
+ narrative:'\x28High: 81, low: 56\nCooler than yesterday.
66
+ }
67
+ {
68
+ currently:{condition:[Rainy']temperature:+\x2D}
69
+ high:+\x38
70
+ location:New York'
71
+ low:-\x02
72
+ narrative:'\x31High: 56, low: -2\nWinter Weather Alert in effect.
73
+ }
74
+ ]
75
+ ```
76
+
77
+ ## Installation
78
+
79
+ Add this line to your application's Gemfile:
80
+
81
+ ```ruby
82
+ gem 'tinycode'
83
+ ```
84
+
85
+ And then execute:
86
+
87
+ ```sh
88
+ $ bundle install
89
+ ```
90
+
91
+ Or install it yourself as:
92
+
93
+ ```sh
94
+ $ gem install tinycode
95
+ ```
96
+
97
+ ## Usage
98
+
99
+ ### Dumping a data structure
100
+
101
+ ```ruby
102
+ require 'tinycode'
103
+
104
+ 42.tinycode
105
+ ['some', 'array'].tinycode
106
+ # and so on
107
+ ```
108
+
109
+ Alternatively you can use `Tinycode.dump`:
110
+
111
+ ```ruby
112
+ # can use 'tinycode/no_core_ext' instead if class extensions are unwanted
113
+ require 'tinycode'
114
+
115
+ Tinycode.dump(42)
116
+ Tinycode.dump(['some', 'array'])
117
+ # and so on
118
+ ```
119
+
120
+ ### Restoring a data structure
121
+
122
+ ```ruby
123
+ require 'tinycode'
124
+
125
+ Tinycode.load("+\x2A".b) # => 42
126
+ Tinycode.load("[some'array']".b) # => ['some', 'array']
127
+ # and so on
128
+ ```
129
+
130
+ ## Development
131
+
132
+ After checking out the repo, run `bundle install` to install dependencies. Then,
133
+ run `rake watch` to run yard, tests, and linting on every file save.
134
+
135
+ To install this gem onto your local machine, run `rake install`.
136
+
137
+ To release a new version, update the version number in `version.rb`.
138
+ Then run `rake release`, which will create/push a git tag for the version and
139
+ publish the `.gem` file to [rubygems.org].
140
+
141
+ [rubygems.org]: https://rubygems.org
142
+
143
+ ## License
144
+
145
+ All copyright rights are held by the authors as recorded in git.
146
+ See the commit history for a comprehensive listing of copyright holders and
147
+ copyright dates.
148
+
149
+ This program is free software: you can redistribute it and/or modify
150
+ it under the terms of the GNU General Public License as published by
151
+ the Free Software Foundation, either version 3 of the License, or
152
+ (at your option) any later version.
153
+
154
+ This program is distributed in the hope that it will be useful,
155
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
156
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
157
+ GNU General Public License for more details.
158
+
159
+ See LICENSE in this repository for the full license text.
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'bundler/gem_tasks'
5
+ require 'rspec/core/rake_task'
6
+ require 'rubocop/rake_task'
7
+ require 'yard'
8
+
9
+ RSpec::Core::RakeTask.new(:spec)
10
+ RuboCop::RakeTask.new(:lint) do |task|
11
+ task.options = %w[--except Metrics]
12
+ end
13
+ YARD::Rake::YardocTask.new
14
+
15
+ task :watch do
16
+ sh 'rerun -p "{**/*.{rb,gemspec},exe/**/*,Gemfile*,Rakefile}" -x rake yard spec lint:auto_correct'
17
+ end
18
+
19
+ task default: [:yard, :spec, :lint]
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Style/Documentation
4
+
5
+
6
+ class Array
7
+ def tinycode
8
+ Tinycode.dump(self)
9
+ end
10
+ end
11
+
12
+ class Hash
13
+ def tinycode
14
+ Tinycode.dump(self)
15
+ end
16
+ end
17
+
18
+ class String
19
+ def tinycode
20
+ Tinycode.dump(self)
21
+ end
22
+ end
23
+
24
+ class Symbol
25
+ def tinycode
26
+ Tinycode.dump(self)
27
+ end
28
+ end
29
+
30
+ class Integer
31
+ def tinycode
32
+ Tinycode.dump(self)
33
+ end
34
+ end
35
+
36
+ class TrueClass
37
+ def tinycode
38
+ Tinycode.dump(self)
39
+ end
40
+ end
41
+
42
+ class FalseClass
43
+ def tinycode
44
+ Tinycode.dump(self)
45
+ end
46
+ end
47
+
48
+ class NilClass
49
+ def tinycode
50
+ Tinycode.dump(self)
51
+ end
52
+ end
53
+
54
+ # rubocop:enable Style/Documentation
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tinycode
4
+ # Mixin module for U8, U16, U32, and U64 classes
5
+ module FixedInteger
6
+ attr_reader :value
7
+ alias to_i value
8
+ alias to_int value
9
+
10
+ def initialize(value)
11
+ raise ArgumentError, "#{value.class} cannot be converted to integer" unless value.respond_to?(:to_int)
12
+
13
+ @value = value.to_int
14
+
15
+ raise RangeError, "#{@value.inspect} out of range for #{self.class}" unless self.class.range.include?(@value)
16
+ end
17
+
18
+ def tinycode
19
+ Tinycode.dump(self)
20
+ end
21
+
22
+ def to_s
23
+ value.to_s
24
+ end
25
+
26
+ def inspect
27
+ "#<#{self.class} value=#{value.inspect}>"
28
+ end
29
+
30
+ def ==(other)
31
+ value == other.value
32
+ end
33
+ end
34
+
35
+ # an unsigned integer that takes up exactly 8 bits
36
+ class U8
37
+ include FixedInteger
38
+ def self.range
39
+ 0x00..0xFF
40
+ end
41
+ end
42
+
43
+ # an unsigned integer that takes up exactly 16 bits
44
+ class U16
45
+ include FixedInteger
46
+ def self.range
47
+ 0x0000..0xFFFF
48
+ end
49
+ end
50
+
51
+ # an unsigned integer that takes up exactly 32 bits
52
+ class U32
53
+ include FixedInteger
54
+ def self.range
55
+ 0x00000000..0xFFFFFFFF
56
+ end
57
+ end
58
+
59
+ # an unsigned integer that takes up exactly 64 bits
60
+ class U64
61
+ include FixedInteger
62
+ def self.range
63
+ 0x00000000_00000000..0xFFFFFFFF_FFFFFFFF
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'fixed_integer'
4
+ require_relative 'version'
5
+
6
+ # An encoder and decoder for tinycode, which is a bencode like encoding that
7
+ # supports more data-types and slightly smaller encoded sizes.
8
+ module Tinycode
9
+ class ParseError < StandardError; end
10
+
11
+ class << self
12
+ def dump(obj)
13
+ case obj
14
+ when Array then dump_array(obj)
15
+ when Hash then dump_hash(obj)
16
+ when String then dump_string(obj)
17
+ when Symbol then dump_symbol(obj)
18
+ when Integer then dump_integer(obj)
19
+ when U8 then dump_fixed_integer(obj, code: '%', pack: 'aC')
20
+ when U16 then dump_fixed_integer(obj, code: '$', pack: 'aS>')
21
+ when U32 then dump_fixed_integer(obj, code: '!', pack: 'aL>')
22
+ when U64 then dump_fixed_integer(obj, code: '#', pack: 'aQ>')
23
+ when nil then "\0".b
24
+ when true then '?T'.b
25
+ when false then '?F'.b
26
+ else raise ArgumentError, "Don't know how to dump #{obj.class}"
27
+ end
28
+ end
29
+
30
+ def load(tinycode)
31
+ raise ArgumentError, "Cannot convert #{Tinycode.class} to String" if !tinycode.respond_to?(:to_str)
32
+
33
+ tinycode = tinycode.to_str.b
34
+ struct, length = load_with_length(tinycode)
35
+ raise ParseError, 'Input is longer than expected' if length != tinycode.length
36
+
37
+ struct
38
+ end
39
+
40
+ private
41
+
42
+ def load_with_length(tinycode)
43
+ case tinycode[0]
44
+ when /[\w \t\n]/ then load_id(tinycode)
45
+ when '[' then load_array(tinycode)
46
+ when '{' then load_hash(tinycode)
47
+ when "'" then load_string(tinycode)
48
+ when ':' then load_symbol(tinycode)
49
+ when '-', '+' then load_integer(tinycode)
50
+ when '%' then load_fixed_integer(tinycode, U8, length: 1, unpack: 'C')
51
+ when '$' then load_fixed_integer(tinycode, U16, length: 2, unpack: 'S>')
52
+ when '!' then load_fixed_integer(tinycode, U32, length: 4, unpack: 'L>')
53
+ when '#' then load_fixed_integer(tinycode, U64, length: 8, unpack: 'Q>')
54
+ when "\0" then [nil, 1]
55
+ when '?' then load_boolean(tinycode)
56
+ else raise ParseError, "Unexpected character: #{tinycode[0]}"
57
+ end
58
+ end
59
+
60
+ def dump_array(ary)
61
+ "[#{ary.map(&method(:dump)).join}]".b
62
+ end
63
+
64
+ def load_array(tinycode)
65
+ offset = 1
66
+ result = []
67
+ until tinycode[offset] == ']'
68
+ value, length = load_with_length(tinycode[offset..])
69
+ result << value
70
+ offset += length
71
+ end
72
+ [result, offset + 1]
73
+ end
74
+
75
+ def dump_hash(hash)
76
+ "{#{hash.sort.flatten(1).map(&method(:dump)).join}}".b
77
+ end
78
+
79
+ def load_hash(tinycode)
80
+ offset = 1
81
+ result = {}
82
+ until tinycode[offset] == '}'
83
+ key, key_length = load_with_length(tinycode[offset..])
84
+ offset += key_length
85
+ value, value_length = load_with_length(tinycode[offset..])
86
+ offset += value_length
87
+ raise if result.key?(key)
88
+
89
+ result[key] = value
90
+ end
91
+ [result, offset + 1]
92
+ end
93
+
94
+ def dump_string(str, code: "'")
95
+ str = str.encode('UTF-8').b unless str.encoding == Encoding::ASCII_8BIT
96
+ if str =~ /\A[\w \t\n]+\z/
97
+ [str, code].pack('a*a')
98
+ else
99
+ [code, str.length, str].pack('awa*')
100
+ end
101
+ end
102
+
103
+ def load_id(tinycode)
104
+ length = tinycode.index(/[':]/)
105
+ value = tinycode[...length]
106
+ value = if tinycode[length] == ':'
107
+ value.to_sym
108
+ else
109
+ value.force_encoding('UTF-8')
110
+ end
111
+ [value, length + 1]
112
+ end
113
+
114
+ def load_string(tinycode)
115
+ str_length, offset = load_varint(tinycode[1..])
116
+ [tinycode[(1 + offset)...(1 + offset + str_length)].force_encoding('UTF-8'), 1 + offset + str_length]
117
+ end
118
+
119
+ def dump_symbol(sym)
120
+ dump_string(sym.name, code: ':')
121
+ end
122
+
123
+ def load_symbol(sym)
124
+ value, offset = load_string(sym)
125
+ [value.to_sym, offset]
126
+ end
127
+
128
+ def dump_integer(num)
129
+ if num < 0
130
+ ['-', -num].pack('aw')
131
+ else
132
+ ['+', num].pack('aw')
133
+ end
134
+ end
135
+
136
+ def load_integer(tinycode)
137
+ value, offset = load_varint(tinycode[1..])
138
+ value = -value if tinycode[0] == '-'
139
+
140
+ [value, 1 + offset]
141
+ end
142
+
143
+ def dump_fixed_integer(num, code:, pack:)
144
+ [code, num].pack(pack)
145
+ end
146
+
147
+ def load_fixed_integer(tinycode, clazz, length:, unpack:)
148
+ [clazz.new(tinycode[1...(1 + length)].unpack1(unpack)), 1 + length]
149
+ end
150
+
151
+ def load_varint(tinycode)
152
+ length = tinycode.each_byte.lazy.take_while {|b| b > 0x7F }.count + 1
153
+ [tinycode[...length].unpack1('w'), length]
154
+ end
155
+
156
+ def load_boolean(tinycode)
157
+ case tinycode[1]
158
+ when 'T' then [true, 2]
159
+ when 'F' then [false, 2]
160
+ else raise ParseError, 'Malformed boolean'
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'module'
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tinycode
4
+ VERSION = '0.1.0'
5
+ end
data/lib/tinycode.rb ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'tinycode/core_ext'
4
+ require_relative 'tinycode/module'
data/tinycode.gemspec ADDED
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/tinycode/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'tinycode'
7
+ spec.version = Tinycode::VERSION
8
+ spec.summary = 'An encoder and decoder for tinycode'
9
+ spec.description = 'tinycode is a bencode like encoding that supports more ' \
10
+ 'data-types and slightly smaller encoded sizes.'
11
+ spec.authors = ['Alex Gittemeier']
12
+ spec.license = 'GPL-3.0'
13
+ spec.email = ['me@a.lexg.dev']
14
+ spec.metadata['source_code_uri'] = 'https://gitlab.com/windows93/tinycode'
15
+ spec.metadata['rubygems_mfa_required'] = 'true'
16
+ # spec.metadata['changelog_uri'] =
17
+
18
+ spec.homepage = spec.metadata['source_code_uri']
19
+ spec.metadata['homepage_uri'] = spec.homepage
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
24
+ `git ls-files -z`.split("\x0").reject do |f|
25
+ (f == __FILE__) || f.match(%r{\A(?:(?:test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
26
+ end
27
+ end
28
+ spec.bindir = 'exe'
29
+ spec.executables = spec.files.grep(%r{\Aexe/}) {|f| File.basename(f) }
30
+ spec.require_paths = ['lib']
31
+
32
+ spec.required_ruby_version = '>= 3.0.0'
33
+
34
+ spec.add_development_dependency 'rake', '~> 13.0'
35
+ spec.add_development_dependency 'rerun', '~> 0.13'
36
+ spec.add_development_dependency 'rspec', '~> 3.0'
37
+ spec.add_development_dependency 'rubocop', '~> 1.21'
38
+ spec.add_development_dependency 'rubocop-rake', '~> 0.6'
39
+ spec.add_development_dependency 'rubocop-rspec', '~> 2.6'
40
+ spec.add_development_dependency 'simplecov', '~> 0.21'
41
+ spec.add_development_dependency 'yard', '~> 0.9'
42
+ end
metadata ADDED
@@ -0,0 +1,173 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tinycode
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Alex Gittemeier
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-11-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '13.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '13.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rerun
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.13'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.13'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.21'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.21'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop-rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.6'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.6'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop-rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '2.6'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '2.6'
97
+ - !ruby/object:Gem::Dependency
98
+ name: simplecov
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.21'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.21'
111
+ - !ruby/object:Gem::Dependency
112
+ name: yard
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '0.9'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '0.9'
125
+ description: tinycode is a bencode like encoding that supports more data-types and
126
+ slightly smaller encoded sizes.
127
+ email:
128
+ - me@a.lexg.dev
129
+ executables: []
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - ".rspec"
134
+ - ".rubocop.yml"
135
+ - Gemfile
136
+ - Gemfile.lock
137
+ - LICENSE
138
+ - README.md
139
+ - Rakefile
140
+ - lib/tinycode.rb
141
+ - lib/tinycode/core_ext.rb
142
+ - lib/tinycode/fixed_integer.rb
143
+ - lib/tinycode/module.rb
144
+ - lib/tinycode/no_core_ext.rb
145
+ - lib/tinycode/version.rb
146
+ - tinycode.gemspec
147
+ homepage: https://gitlab.com/windows93/tinycode
148
+ licenses:
149
+ - GPL-3.0
150
+ metadata:
151
+ source_code_uri: https://gitlab.com/windows93/tinycode
152
+ rubygems_mfa_required: 'true'
153
+ homepage_uri: https://gitlab.com/windows93/tinycode
154
+ post_install_message:
155
+ rdoc_options: []
156
+ require_paths:
157
+ - lib
158
+ required_ruby_version: !ruby/object:Gem::Requirement
159
+ requirements:
160
+ - - ">="
161
+ - !ruby/object:Gem::Version
162
+ version: 3.0.0
163
+ required_rubygems_version: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - ">="
166
+ - !ruby/object:Gem::Version
167
+ version: '0'
168
+ requirements: []
169
+ rubygems_version: 3.2.22
170
+ signing_key:
171
+ specification_version: 4
172
+ summary: An encoder and decoder for tinycode
173
+ test_files: []