compactdata 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|