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 +7 -0
- data/.DS_Store +0 -0
- data/.gitignore +15 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +12 -0
- data/LICENSE.txt +21 -0
- data/README.md +55 -0
- data/Rakefile +8 -0
- data/compactdata.gemspec +26 -0
- data/lib/compactdata/interpreter.rb +20 -0
- data/lib/compactdata/model/model.rb +264 -0
- data/lib/compactdata/parser/parser.rb +294 -0
- data/lib/compactdata/tokeniser/context.rb +117 -0
- data/lib/compactdata/tokeniser/tokeniser.rb +28 -0
- data/lib/compactdata/util/functions.rb +74 -0
- data/lib/compactdata/util/unicode.rb +44 -0
- data/lib/compactdata/version.rb +5 -0
- data/lib/compactdata.rb +9 -0
- metadata +88 -0
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
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
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
data/compactdata.gemspec
ADDED
@@ -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
|
data/lib/compactdata.rb
ADDED
@@ -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: []
|