compactdata 0.0.6

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c7a0c33715643aef4cce1e216b64a24536a75ab4c6cd4f0c274fc12b75b46351
4
+ data.tar.gz: eda5c5dcf280d1ab706a02018932b8afef8ece3582d2bfaec8d2d5f5550c470e
5
+ SHA512:
6
+ metadata.gz: cb7c8d70c740071124fc27faf9f2bf45707c6c6de71037deb1469914c3a7e3ecf078b9b00fe78a193cb68227bb687aa8cc81e322275c4e431d7d9c171fb9c67a
7
+ data.tar.gz: 69d14879fdcaca94c22f94f7707ae64f6fdbf89b5b0338c98fc18c02b6b7ded1f5c8daf2bdac4ec2b413e94440203609e834683dd48e497fe48cd636c2d3f25a
data/.DS_Store ADDED
Binary file
data/.gitignore ADDED
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ .idea
10
+ *.gem
11
+
12
+ # rspec failure tracking
13
+ .rspec_status
14
+
15
+ Gemfile.lock
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ 0.0.1
2
+ ===
3
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in compactdata.gemspec
6
+ group :development, :test do
7
+ gem 'debase'
8
+ gem 'ruby-debug-ide'
9
+ gem 'pry'
10
+ end
11
+
12
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 NUM Technology Ltd
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # CompactData Parser
2
+
3
+ This Ruby gem is a CompactData parser and requires Ruby 2.6.0.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'compactdata'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle install
16
+
17
+ Or clone the repository and build and install it yourself as:
18
+
19
+ $ rake install
20
+
21
+ or if that fails:
22
+
23
+ $ sudo rake install
24
+
25
+ ## Usage
26
+
27
+ In `irb`:
28
+
29
+ ```ruby
30
+ 2.6.0 :001 > require 'compactdata'
31
+ => true
32
+ 2.6.0 :002 > CompactData.parse 'a=b'
33
+ => {"a"=>"b"}
34
+ 2.6.0 :003 >
35
+ ```
36
+ In a Ruby file:
37
+
38
+ ```ruby
39
+ require 'compactdata'
40
+
41
+ str='s=CompactData'
42
+ pretty = true
43
+ result = CompactData.parse str
44
+ puts result
45
+
46
+ ```
47
+ where `str` is the CompactData to be parsed.
48
+
49
+ ## Development
50
+
51
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
52
+
53
+ ## License
54
+
55
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,26 @@
1
+ lib = File.expand_path('lib', __dir__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'COMPACTDATA/VERSION'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'compactdata'
7
+ spec.version = COMPACTDATA::VERSION
8
+ spec.authors = ['Elliott Brown']
9
+ spec.email = ['mail@elliottinvent.com']
10
+
11
+ spec.summary = 'A Ruby parser for the CompactData serialisation format.'
12
+ spec.homepage = 'https://gitlab.com/NUMTechnology/CompactData/Libraries/compactdata-ruby'
13
+ spec.license = 'MIT'
14
+
15
+ # Specify which files should be added to the gem when it is released.
16
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
17
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
18
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
19
+ end
20
+ spec.bindir = 'exe'
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ['lib']
23
+
24
+ spec.add_development_dependency 'rake', '~> 12.3'
25
+ spec.add_development_dependency 'rspec', '~> 3.0'
26
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ # The CompactData namespace
6
+ module CompactData
7
+ # Interpreter-specific errors
8
+ class ParserError < StandardError
9
+ end
10
+
11
+ # CompactData String to Ruby Hash/Array/primitive
12
+ def self.parse(str)
13
+ CompactData::Parser.parse_compactdata(str).to_j
14
+ end
15
+
16
+ # Ruby Hash/Array/primitive to CompactData
17
+ def self.generate(obj)
18
+ CompactData::Model::CompactData.new(CompactData::Model.to_compactdata(obj)).to_m
19
+ end
20
+ end
@@ -0,0 +1,264 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CompactData
4
+ # A Model module for CompactData
5
+ module Model
6
+ # The root of a CompactData object
7
+ class CompactData
8
+ attr_reader :value
9
+
10
+ def initialize(value)
11
+ @value = value
12
+ end
13
+
14
+ def to_s
15
+ if @value.instance_of? Array
16
+ @value.map(&:to_s)
17
+ else
18
+ "CompactData: #{@value}"
19
+ end
20
+ end
21
+
22
+ def to_j
23
+ @value.nil? ? 'null' : @value.to_j
24
+ end
25
+
26
+ def to_m
27
+ if @value.instance_of?(CompactDataMap)
28
+ @value.items.map(&:to_m).join(';')
29
+ else
30
+ @value.to_m
31
+ end
32
+ end
33
+ end
34
+
35
+ # A CompactData Array
36
+ class CompactDataArray
37
+ attr_reader :items
38
+
39
+ def initialize(items)
40
+ @items = items
41
+ end
42
+
43
+ def to_s
44
+ items = @items.map(&:to_s)
45
+ "CompactDataArray: #{items}"
46
+ end
47
+
48
+ def to_j
49
+ @items.map(&:to_j)
50
+ end
51
+
52
+ def to_m
53
+ list = @items.map(&:to_m).join ';'
54
+ "[#{list}]"
55
+ end
56
+ end
57
+
58
+ # A CompactData Map
59
+ class CompactDataMap
60
+ attr_reader :items
61
+
62
+ def initialize(items)
63
+ @items = items
64
+ end
65
+
66
+ def to_s
67
+ items = @items.map(&:to_s)
68
+ "CompactDataMap: #{items}"
69
+ end
70
+
71
+ def to_j
72
+ result = @items.map { |p| [p.key, p.value.to_j] }
73
+ result.to_h
74
+ end
75
+
76
+ def to_m
77
+ compactdata = @items.map(&:to_m).join ';'
78
+ "(#{compactdata})"
79
+ end
80
+ end
81
+
82
+ # A CompactData Primitive
83
+ class CompactDataPrimitive
84
+ attr_reader :value
85
+
86
+ def initialize(value)
87
+ @value = value
88
+ end
89
+
90
+ def to_s
91
+ "CompactDataPrimitive: #{@value}"
92
+ end
93
+
94
+ def to_m
95
+ @value.to_s
96
+ end
97
+ end
98
+
99
+ # A CompactData PAir
100
+ class CompactDataPair
101
+ attr_reader :key, :value
102
+
103
+ def initialize(key, value)
104
+ @key = key
105
+ @value = value
106
+ end
107
+
108
+ def to_s
109
+ "CompactDataPair: #{key}=#{@value}"
110
+ end
111
+
112
+ def to_j
113
+ { @key => @value.to_j }
114
+ end
115
+
116
+ def to_m
117
+ key = UTIL.non_string_primitive?(@key) ? "\"#{@key}\"" : @key
118
+ if @value.instance_of?(CompactDataPair)
119
+ "#{key}(#{@value.to_m})"
120
+ elsif @value.instance_of?(CompactDataMap) || @value.instance_of?(CompactDataArray)
121
+ "#{key}#{@value.to_m}"
122
+ else
123
+ "#{key}=#{@value.to_m}"
124
+ end
125
+ end
126
+ end
127
+
128
+ # A CompactData FLOAT
129
+ class CompactDataFloat
130
+ attr_reader :value
131
+
132
+ def initialize(value)
133
+ @value = value
134
+ end
135
+
136
+ def to_s
137
+ "CompactDataFloat: #{@value}"
138
+ end
139
+
140
+ def to_j
141
+ @value
142
+ end
143
+
144
+ def to_m
145
+ format('%.16G', @value).sub('E+', 'E')
146
+ end
147
+ end
148
+
149
+ # A CompactData Integer
150
+ class CompactDataInteger
151
+ attr_reader :value
152
+
153
+ def initialize(value)
154
+ @value = value
155
+ end
156
+
157
+ def to_s
158
+ "CompactDataInteger: #{@value}"
159
+ end
160
+
161
+ def to_j
162
+ @value
163
+ end
164
+
165
+ def to_m
166
+ @value.to_s
167
+ end
168
+ end
169
+
170
+ # A CompactData String
171
+ class CompactDataString
172
+ attr_reader :value
173
+
174
+ def initialize(value)
175
+ @value = value
176
+ end
177
+
178
+ def to_s
179
+ "CompactDataString: #{@value}"
180
+ end
181
+
182
+ def to_j
183
+ @value
184
+ end
185
+
186
+ def to_m
187
+ UTIL.escape_and_quote @value
188
+ end
189
+ end
190
+
191
+ # A CompactData Quoted String
192
+ class CompactDataQuoted
193
+ attr_reader :value
194
+
195
+ def initialize(value)
196
+ @value = value
197
+ end
198
+
199
+ def to_s
200
+ "CompactDataQuoted: #{@value}"
201
+ end
202
+
203
+ def to_j
204
+ @value
205
+ end
206
+
207
+ def to_m
208
+ UTIL.escape_and_quote @value
209
+ end
210
+ end
211
+
212
+ # A CompactData Boolean or NULL
213
+ class CompactDataBoolNull
214
+ def initialize(value)
215
+ @value = value
216
+ end
217
+
218
+ CompactData_NULL = CompactDataBoolNull.new nil
219
+ CompactData_TRUE = CompactDataBoolNull.new true
220
+ CompactData_FALSE = CompactDataBoolNull.new false
221
+
222
+ def to_s
223
+ "CompactDataBoolNull: #{@value}"
224
+ end
225
+
226
+ def to_j
227
+ @value
228
+ end
229
+
230
+ def to_m
231
+ @value.nil? ? 'null' : @value
232
+ end
233
+ end
234
+
235
+ # Convert a Ruby Hash/Array/Primitive to a CompactData object.
236
+ def self.to_compactdata(obj)
237
+ if obj.instance_of? Float
238
+ CompactDataFloat.new obj
239
+ elsif obj.instance_of? Integer
240
+ CompactDataInteger.new obj
241
+ elsif obj.instance_of? String
242
+ CompactDataString.new obj
243
+ elsif obj.instance_of? Hash
244
+ if obj.keys.length == 1
245
+ key = obj.keys[0]
246
+ CompactDataPair.new key, to_compactdata(obj[key])
247
+ else
248
+ CompactDataMap.new(obj.keys.map { |k| CompactDataPair.new k, to_compactdata(obj[k]) })
249
+ end
250
+ elsif obj.instance_of? Array
251
+ CompactDataArray.new(obj.map { |i| to_compactdata i })
252
+ elsif obj.instance_of? TrueClass
253
+ CompactDataBoolNull::CompactData_TRUE
254
+ elsif obj.instance_of? FalseClass
255
+ CompactDataBoolNull::CompactData_FALSE
256
+ elsif obj.instance_of? NilClass
257
+ CompactDataBoolNull::CompactData_NULL
258
+ else
259
+ puts "Cannot handle #{obj}"
260
+ exit
261
+ end
262
+ end
263
+ end
264
+ end
@@ -0,0 +1,294 @@
1
+ # frozen_string_literal: true
2
+ module CompactData
3
+ # A Parser module
4
+ module Parser
5
+ REPLACEMENTS = {
6
+ '\t' => "\t",
7
+ '\n' => "\n",
8
+ '\b' => "\b",
9
+ '\f' => "\f",
10
+ '\r' => "\r",
11
+ '~t' => "\t",
12
+ '~n' => "\n",
13
+ '~b' => "\b",
14
+ '~f' => "\f",
15
+ '~r' => "\r",
16
+ '~\\' => '\\',
17
+ '\\\\' => '\\',
18
+ '~~' => '~',
19
+ '\~' => '~',
20
+ '~(' => '(',
21
+ '\(' => '(',
22
+ '~)' => ')',
23
+ '\)' => ')',
24
+ '~}' => '}',
25
+ '\}' => '}',
26
+ '~[' => '[',
27
+ '\[' => '[',
28
+ '~]' => ']',
29
+ '\]' => ']',
30
+ '~;' => ';',
31
+ '\;' => ';',
32
+ '~`' => '`',
33
+ '\`' => '`',
34
+ '~"' => '"',
35
+ '"' => '"',
36
+ '~=' => '=',
37
+ '\=' => '='
38
+ }.freeze
39
+
40
+ def self.parse_compactdata(str)
41
+ tokens = CompactData::Tokeniser.tokenise str
42
+ if tokens.nil? || tokens.empty?
43
+ CompactData::Model::CompactData.new nil
44
+ elsif root_primitive? tokens[0]
45
+ CompactData::Model::CompactData.new tokens[0].value
46
+ else
47
+ result = parse_structures(tokens)
48
+ return CompactData::Model::CompactData.new result[0] if result.length == 1
49
+
50
+ return CompactData::Model::CompactData.new CompactData::Model::CompactDataMap.new(result) if all_pairs?(result)
51
+
52
+ CompactData::Model::CompactData.new CompactData::Model::CompactDataArray.new(result)
53
+ end
54
+ end
55
+
56
+ def self.root_primitive?(token)
57
+ %i[string quoted null true false integer float].include?(token)
58
+ end
59
+
60
+ def self.parse_structures(tokens)
61
+ result = []
62
+ until tokens.empty?
63
+ result.push parse_compactdata_value(tokens)
64
+ expect_separator = tokens.shift
65
+ if !expect_separator.nil? && expect_separator.type != :struct_sep
66
+ raise ParserError, "Expected ';' near #{tokens}"
67
+ end
68
+ end
69
+ result
70
+ end
71
+
72
+ def self.parse_compactdata_value(tokens)
73
+ first_token = tokens.shift
74
+
75
+ case first_token.type
76
+ when :lbracket
77
+ tokens.unshift first_token
78
+ return parse_compactdata_array tokens
79
+ when :lparen
80
+ tokens.unshift first_token
81
+ return parse_compactdata_map tokens
82
+ when :string, :quoted
83
+ peek = tokens[0]
84
+ key = first_token.value
85
+ if !peek.nil? && peek.type == :equals
86
+ tokens.shift
87
+ return CompactData::Model::CompactDataPair.new replace_escapes(unquote(key)), parse_pair_value(tokens)
88
+ end
89
+
90
+ if !peek.nil? && (peek.type == :lbracket || peek.type == :lparen)
91
+ return CompactData::Model::CompactDataPair.new replace_escapes(unquote(key)), parse_pair_value(tokens)
92
+ end
93
+
94
+ if peek.nil? || peek.type == :struct_sep || peek.type == :rparen || peek.type == :rbracket
95
+ return CompactData::Model::CompactDataString.new(replace_escapes(first_token.value)) if first_token.type == :string
96
+
97
+ return CompactData::Model::CompactDataQuoted.new(replace_escapes(unquote(first_token.value)))
98
+ end
99
+ when :integer
100
+ return CompactData::Model::CompactDataInteger.new first_token.value
101
+ when :float
102
+ return CompactData::Model::CompactDataFloat.new first_token.value
103
+ when :null
104
+ return CompactData::Model::CompactDataBoolNull::CompactData_NULL
105
+ when true
106
+ return CompactData::Model::CompactDataBoolNull::CompactData_TRUE
107
+ when false
108
+ return CompactData::Model::CompactDataBoolNull::CompactData_FALSE
109
+ else
110
+ tokens.unshift first_token
111
+ maybe_primitive = parse_primitive tokens
112
+ return maybe_primitive unless maybe_primitive.nil?
113
+ end
114
+ raise ParserError, "Unexpected token: \'#{first_token}\'"
115
+ end
116
+
117
+ def self.parse_pair_value(tokens)
118
+ first_token = tokens.shift
119
+ case first_token.type
120
+ when :lbracket
121
+ tokens.unshift first_token
122
+ return parse_compactdata_array tokens
123
+ when :lparen
124
+ tokens.unshift first_token
125
+ return parse_compactdata_map tokens
126
+ when :string, :quoted
127
+ peek = tokens[0]
128
+
129
+ raise ParserError, "Unexpected token: \'#{first_token}\'" if !peek.nil? && peek.type == :equals
130
+
131
+ if !peek.nil? && (peek.type == :lbracket || peek.type == :lparen)
132
+ raise ParserError, "Unexpected token: \'#{first_token}\'"
133
+ end
134
+
135
+ if peek.nil? || peek.type == :struct_sep || peek.type == :rparen || peek.type == :rbracket
136
+ return CompactData::Model::CompactDataString.new replace_escapes(first_token.value) if first_token.type == :string
137
+
138
+ return CompactData::Model::CompactDataQuoted.new replace_escapes(unquote(first_token.value))
139
+ end
140
+ raise ParserError, "Unexpected token: \'#{first_token}\'"
141
+ when :integer
142
+ return CompactData::Model::CompactDataInteger.new first_token.value
143
+ when :float
144
+ return CompactData::Model::CompactDataFloat.new first_token.value
145
+ when :null
146
+ return CompactData::Model::CompactDataBoolNull::CompactData_NULL
147
+ when true
148
+ return CompactData::Model::CompactDataBoolNull::CompactData_TRUE
149
+ when false
150
+ return CompactData::Model::CompactDataBoolNull::CompactData_FALSE
151
+ else
152
+ tokens.unshift first_token
153
+ maybe_primitive = parse_primitive tokens
154
+ return maybe_primitive unless maybe_primitive.nil?
155
+ end
156
+ raise ParserError, "Unexpected token: \'#{first_token}\'"
157
+ end
158
+
159
+ def self.parse_primitive(tokens)
160
+ result = nil
161
+ tok = tokens.shift
162
+
163
+ case tok.type
164
+ when :lparen, :rparen, :lbracket, :rbracket, :equals
165
+ raise ParserError, "Unexpected token: \'#{tok}\'" if tokens.empty?
166
+
167
+ tokens.unshift tok
168
+ return nil
169
+ when :null
170
+ result = CompactData::Model::CompactDataBoolNull::CompactData_NULL
171
+ when true
172
+ result = CompactData::Model::CompactDataBoolNull::CompactData_TRUE
173
+ when false
174
+ result = CompactData::Model::CompactDataBoolNull::CompactData_FALSE
175
+ when :quoted
176
+ result = CompactData::Model::CompactDataQuoted.new replace_escapes(unquote(tok.value))
177
+ when :string
178
+ result = CompactData::Model::CompactDataString.new replace_escapes(tok.value)
179
+ when :integer
180
+ result = CompactData::Model::CompactDataInteger.new tok.value
181
+ when :float
182
+ result = CompactData::Model::CompactDataFloat.new tok.value
183
+ else
184
+ raise ParserError, "Unknown token type in: \'#{tok}\'"
185
+ end
186
+
187
+ peek = tokens[0]
188
+ raise ParserError, 'Only one primitive allowed at the root.' if !peek.nil? && peek.type == :struct_sep
189
+
190
+ if !peek.nil? && (peek.type == :lparen || peek.type == :lbracket || peek.type == :equals)
191
+ s.unshift tok
192
+ return nil
193
+ elsif !peek.nil?
194
+ raise ParserError, "Unexpected token: \'#{peek}\'"
195
+ end
196
+ result
197
+ end
198
+
199
+ def self.parse_compactdata_map(tokens)
200
+ first_token = tokens.shift
201
+ entries = []
202
+
203
+ until tokens.empty?
204
+ peek = tokens[0]
205
+ if !peek.nil? && peek.type == :rparen
206
+ tokens.shift
207
+ break
208
+ end
209
+
210
+ mp = parse_compactdata_value tokens
211
+ entries.push mp
212
+
213
+ peek = tokens[0]
214
+
215
+ raise ParserError, "Expected ')' near #{first_token}" if peek.nil?
216
+
217
+ case peek.type
218
+ when :rparen
219
+ tokens.shift
220
+ break
221
+ when :struct_sep
222
+ tokens.shift
223
+ peek = tokens[0]
224
+ raise ParserError, "Unexpected ; before ] at #{peek}" if !peek.nil? && peek.type == :rparen
225
+ end
226
+ end
227
+ CompactData::Model::CompactDataMap.new entries
228
+ end
229
+
230
+ def self.parse_compactdata_array(tokens)
231
+ first_token = tokens.shift
232
+ entries = []
233
+
234
+ until tokens.empty?
235
+ peek = tokens[0]
236
+ if !peek.nil? && peek.type == :rbracket
237
+ tokens.shift
238
+ break
239
+ end
240
+
241
+ mp = parse_compactdata_value tokens
242
+ entries.push mp
243
+
244
+ peek = tokens[0]
245
+
246
+ raise ParserError, "Expected ']' near #{first_token}" if peek.nil?
247
+
248
+ case peek.type
249
+ when :rbracket
250
+ tokens.shift
251
+ break
252
+ when :struct_sep
253
+ tokens.shift
254
+ peek = tokens[0]
255
+ raise ParserError, "Unexpected ; before ] at #{peek}" if !peek.nil? && peek.type == :rparen
256
+ end
257
+ end
258
+ CompactData::Model::CompactDataArray.new entries
259
+ end
260
+
261
+ def self.unquote(str)
262
+ return str unless str.instance_of?(String)
263
+
264
+ if (str.start_with?('`') && str.end_with?('`')) || (str.start_with?('"') && str.end_with?('"'))
265
+ str[1..-2]
266
+ else
267
+ str
268
+ end
269
+ end
270
+
271
+ def self.replace_escapes(str)
272
+ return str unless str.instance_of?(String)
273
+
274
+ result = str
275
+ i = 0
276
+ while i < str.length
277
+ REPLACEMENTS.each_pair do |key, value|
278
+ if result.slice(i..).start_with?(key)
279
+ result = result.sub(key, value)
280
+ break
281
+ end
282
+ end
283
+ i += 1
284
+ end
285
+ result
286
+ end
287
+
288
+ def self.all_pairs?(arr)
289
+ result = true
290
+ arr.each { |i| result = false unless i.instance_of? CompactData::Model::CompactDataPair }
291
+ result
292
+ end
293
+ end
294
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CompactData
4
+ module Tokeniser
5
+ # A parsing context for a CompactData String
6
+ class Context
7
+ WS = " \t\r\n"
8
+ INTEGER_REGEX = '^-?(?:0|[1-9]\d*)$'
9
+ FLOAT_REGEX = '^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$'
10
+ NON_STRING_TOKENS = '[]();"=`'
11
+
12
+ def initialize(str)
13
+ @str = str
14
+ @tokens = []
15
+ @tok_start = 0
16
+ end
17
+
18
+ def parse
19
+ while next_token
20
+ end
21
+ @tokens
22
+ end
23
+
24
+ def next_token
25
+ while @tok_start < @str.length
26
+ break unless WS.include?(@str[@tok_start])
27
+
28
+ @tok_start += 1
29
+ end
30
+
31
+ return false if @tok_start >= @str.length
32
+
33
+ case @str[@tok_start]
34
+ when '('
35
+ tok_type = :lparen
36
+ tok_end = @tok_start + 1
37
+ when ')'
38
+ tok_type = :rparen
39
+ tok_end = @tok_start + 1
40
+ when '['
41
+ tok_type = :lbracket
42
+ tok_end = @tok_start + 1
43
+ when ']'
44
+ tok_type = :rbracket
45
+ tok_end = @tok_start + 1
46
+ when ';'
47
+ tok_type = :struct_sep
48
+ tok_end = @tok_start + 1
49
+ when '='
50
+ tok_type = :equals
51
+ tok_end = @tok_start + 1
52
+ when '"'
53
+ tok_type = :quoted
54
+ tok_end = scan_to_end_of_quoted(@str, @tok_start, '"')
55
+ when '`'
56
+ tok_type = :quoted
57
+ tok_end = scan_to_end_of_quoted(@str, @tok_start, '`')
58
+ when '{'
59
+ ## Braced strings must include the braces in the string
60
+ tok_type = :quoted
61
+ tok_end = scan_to_end_of_quoted(@str, @tok_start, '}')
62
+ else
63
+ tok_type = :string
64
+ tok_end = scan_to_end_of_string(@str, @tok_start)
65
+ end
66
+
67
+ tok_value = @str[@tok_start..(tok_end - 1)].strip
68
+
69
+ if tok_value.match? INTEGER_REGEX
70
+ number = tok_value.to_i
71
+ @tokens.push Token.new(:integer, number, @tok_start, tok_end)
72
+ elsif tok_value.match? FLOAT_REGEX
73
+ number = tok_value.to_f
74
+ @tokens.push Token.new(:float, number, @tok_start, tok_end)
75
+ elsif tok_value == 'null'
76
+ @tokens.push Token.new(:null, nil, @tok_start, tok_end)
77
+ elsif tok_value == 'true'
78
+ @tokens.push Token.new(true, true, @tok_start, tok_end)
79
+ elsif tok_value == 'false'
80
+ @tokens.push Token.new(false, false, @tok_start, tok_end)
81
+ else
82
+ @tokens.push Token.new(tok_type, tok_value, @tok_start, tok_end)
83
+ end
84
+
85
+ @tok_start = tok_end
86
+ tok_end < @str.length
87
+ end
88
+
89
+ def scan_to_end_of_quoted(str, start, quote_char)
90
+ end_str = start + 1
91
+ while end_str < str.length
92
+ end_char = str[end_str]
93
+ prev_char = str[end_str - 1]
94
+
95
+ break if end_char == quote_char && prev_char != '\\' && prev_char != '~'
96
+
97
+ end_str += 1
98
+ end
99
+ end_str + 1
100
+ end
101
+
102
+ def scan_to_end_of_string(str, start)
103
+ end_str = start + 1
104
+ while end_str < str.length
105
+ break if NON_STRING_TOKENS.include?(str[end_str]) && !escaped?(str, end_str - 1)
106
+
107
+ end_str += 1
108
+ end
109
+ end_str
110
+ end
111
+
112
+ def escaped?(str, pos)
113
+ pos >= 0 && (str[pos] == '~' || str[pos] == '\\')
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'compactdata/tokeniser/context'
4
+
5
+ module CompactData
6
+ # A CompactData Tokeniser module
7
+ module Tokeniser
8
+ # A class to represent CompactData tokens
9
+ class Token
10
+ attr_reader :type, :value, :from, :to
11
+
12
+ def initialize(type, value, from, to)
13
+ @type = type
14
+ @value = value
15
+ @from = from
16
+ @to = to
17
+ end
18
+
19
+ def to_s
20
+ "type: #{@type}, from: #{@from}, to: #{@to}, value: \"#{@value}\""
21
+ end
22
+ end
23
+
24
+ def self.tokenise(str)
25
+ Context.new(str).parse
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CompactData
4
+ # Helper functions for CompactData.generate
5
+ module UTIL
6
+ SHOULD_BE_GRAVE_QUOTED = /.*[()\[\];{}="].*/.freeze
7
+ IS_NUMERIC = /^-?[0-9]*\.?[0-9]+(?:[Ee][+-]?[0-9]+)?$/.freeze
8
+
9
+ def self.escape_graves(str)
10
+ return str unless str
11
+
12
+ str.gsub(/`/, '~`')
13
+ end
14
+
15
+ def self.escape_double_quotes(str)
16
+ return str unless str
17
+
18
+ i = str.index('"', 1) # ignore the initial quote
19
+ result = str
20
+ # Don't affect the final quote in the string
21
+ while !i.nil? && i < (result.length - 1)
22
+ result = result.slice(0..(i - 1)) + '~u0022' + result.slice((i + 1)..)
23
+ i = result.index('"', 1)
24
+ end
25
+ result
26
+ end
27
+
28
+ def self.grave_quote_if_necessary(str)
29
+ return str unless str
30
+
31
+ if str.match?(SHOULD_BE_GRAVE_QUOTED) ||
32
+ (str.match?(IS_NUMERIC) && str != '00' && str != '01' && str != '000') ||
33
+ (str.strip.empty? ||
34
+ str.match?(IS_NUMERIC) ||
35
+ str == 'true' ||
36
+ str == 'false' ||
37
+ str == 'null')
38
+ "`#{str}`"
39
+ else
40
+ str
41
+ end
42
+ end
43
+
44
+ def self.double_quote_if_necessary(str)
45
+ if str && str.include?('~`')
46
+ "\"#{str}\""
47
+ else
48
+ str
49
+ end
50
+ end
51
+
52
+ def self.replace_nbsp(str)
53
+ str.gsub('\u00a0', ' ')
54
+ end
55
+
56
+ def self.escape_and_quote(str)
57
+ double_quote_if_necessary(
58
+ grave_quote_if_necessary(
59
+ escape_graves(
60
+ UNICODE.escape(
61
+ replace_nbsp(
62
+ escape_double_quotes(str)
63
+ )
64
+ )
65
+ )
66
+ )
67
+ )
68
+ end
69
+
70
+ def self.non_string_primitive?(str)
71
+ str == 'true' || str == 'false' || str == 'null' || str.match?(IS_NUMERIC)
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: false
2
+
3
+ module CompactData
4
+ # Handle escaping of unicode characters.
5
+ module UNICODE
6
+ VERTICAL_TAB = 0x0b
7
+ BACKSPACE = 0x08
8
+ FORMFEED = 0x0c
9
+ LINEFEED = 0x0a
10
+ CARRIAGE_RETURN = 0x0d
11
+ TAB = 0x09
12
+
13
+ def self.escape_char(chr)
14
+ return '' unless chr
15
+
16
+ if chr >= 32 && chr <= 127
17
+ '' << chr
18
+ elsif chr == VERTICAL_TAB
19
+ '~u000B'
20
+ elsif chr == BACKSPACE
21
+ '\\b'
22
+ elsif chr == FORMFEED
23
+ '\\f'
24
+ elsif chr == LINEFEED
25
+ '\\n'
26
+ elsif chr == CARRIAGE_RETURN
27
+ '\\r'
28
+ elsif chr == TAB
29
+ '\\t'
30
+ else
31
+ '' << chr
32
+ end
33
+ end
34
+
35
+ def self.escape(str)
36
+ result = ''
37
+
38
+ str.each_codepoint do |c|
39
+ result << escape_char(c)
40
+ end
41
+ result
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module COMPACTDATA
4
+ VERSION = '0.0.6'
5
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'compactdata/version'
4
+ require 'compactdata/model/model'
5
+ require 'compactdata/tokeniser/tokeniser'
6
+ require 'compactdata/parser/parser'
7
+ require 'compactdata/util/unicode'
8
+ require 'compactdata/util/functions'
9
+ require 'compactdata/interpreter'
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: compactdata
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.6
5
+ platform: ruby
6
+ authors:
7
+ - Elliott Brown
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-03-02 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: '12.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '12.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.0'
41
+ description:
42
+ email:
43
+ - mail@elliottinvent.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".DS_Store"
49
+ - ".gitignore"
50
+ - CHANGELOG.md
51
+ - Gemfile
52
+ - LICENSE.txt
53
+ - README.md
54
+ - Rakefile
55
+ - compactdata.gemspec
56
+ - lib/compactdata.rb
57
+ - lib/compactdata/interpreter.rb
58
+ - lib/compactdata/model/model.rb
59
+ - lib/compactdata/parser/parser.rb
60
+ - lib/compactdata/tokeniser/context.rb
61
+ - lib/compactdata/tokeniser/tokeniser.rb
62
+ - lib/compactdata/util/functions.rb
63
+ - lib/compactdata/util/unicode.rb
64
+ - lib/compactdata/version.rb
65
+ homepage: https://gitlab.com/NUMTechnology/CompactData/Libraries/compactdata-ruby
66
+ licenses:
67
+ - MIT
68
+ metadata: {}
69
+ post_install_message:
70
+ rdoc_options: []
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ requirements: []
84
+ rubygems_version: 3.2.3
85
+ signing_key:
86
+ specification_version: 4
87
+ summary: A Ruby parser for the CompactData serialisation format.
88
+ test_files: []