modl 0.0.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.gitignore +15 -0
- data/.idea/vcs.xml +6 -0
- data/.rspec +3 -0
- data/.rubocop.yml +5 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +4 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +21 -0
- data/README.md +52 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/grammar_tests/1.modl +1 -0
- data/grammar_tests/2.modl +1 -0
- data/grammar_tests/3.modl +1 -0
- data/grammar_tests/a.modl +1 -0
- data/grammar_tests/b.modl +1 -0
- data/grammar_tests/base_tests.json +996 -0
- data/grammar_tests/c.modl +1 -0
- data/grammar_tests/demo_config.modl +9 -0
- data/grammar_tests/error_tests.json +70 -0
- data/grammar_tests/import_config.modl +9 -0
- data/grammar_tests/test_import_dir/nested_import1.txt +1 -0
- data/grammar_tests/test_import_dir/nested_import2.txt +1 -0
- data/grammar_tests/test_import_dir/nested_import3.txt +1 -0
- data/grammar_tests/test_import_dir/test_import.txt +9 -0
- data/lib/modl/interpreter.rb +10 -0
- data/lib/modl/parser/MODLLexer.interp +136 -0
- data/lib/modl/parser/MODLLexer.rb +324 -0
- data/lib/modl/parser/MODLLexer.tokens +41 -0
- data/lib/modl/parser/MODLParser.interp +95 -0
- data/lib/modl/parser/MODLParser.rb +2504 -0
- data/lib/modl/parser/MODLParser.tokens +41 -0
- data/lib/modl/parser/MODLParserBaseListener.rb +164 -0
- data/lib/modl/parser/MODLParserBaseVisitor.rb +107 -0
- data/lib/modl/parser/MODLParserListener.rb +151 -0
- data/lib/modl/parser/MODLParserVisitor.rb +56 -0
- data/lib/modl/parser/class_processor.rb +159 -0
- data/lib/modl/parser/evaluator.rb +164 -0
- data/lib/modl/parser/file_importer.rb +64 -0
- data/lib/modl/parser/global_parse_context.rb +249 -0
- data/lib/modl/parser/instruction_processor.rb +58 -0
- data/lib/modl/parser/interpreter.rb +38 -0
- data/lib/modl/parser/modl_class.rb +102 -0
- data/lib/modl/parser/modl_index.rb +30 -0
- data/lib/modl/parser/modl_keylist.rb +43 -0
- data/lib/modl/parser/modl_method.rb +132 -0
- data/lib/modl/parser/object_cache.rb +54 -0
- data/lib/modl/parser/parsed.rb +1410 -0
- data/lib/modl/parser/parser.rb +42 -0
- data/lib/modl/parser/ref_processor.rb +139 -0
- data/lib/modl/parser/substitutions.rb +67 -0
- data/lib/modl/parser/sutil.rb +78 -0
- data/lib/modl/parser/throwing_error_listener.rb +20 -0
- data/lib/modl/parser/version.rb +5 -0
- data/modl.gemspec +32 -0
- metadata +138 -11
- data/lib/modl.rb +0 -5
@@ -0,0 +1,164 @@
|
|
1
|
+
module Modl
|
2
|
+
module Parser
|
3
|
+
# Evaluate a conditional expression
|
4
|
+
class Evaluator
|
5
|
+
# Evaluate the given condition
|
6
|
+
def self.evaluate(global, condition)
|
7
|
+
return false if global.nil? || !global.is_a?(GlobalParseContext) || !condition.is_a?(Modl::Parser::Parsed::ParsedCondition)
|
8
|
+
|
9
|
+
start = 0
|
10
|
+
if condition.text
|
11
|
+
value1, success = value(global, condition.text)
|
12
|
+
else
|
13
|
+
start = 1
|
14
|
+
value1, success = value(global, condition.values[0].text)
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
# Handle single-value conditions of the form '{x?}'
|
19
|
+
if condition.values.length == start
|
20
|
+
return false if value1.nil?
|
21
|
+
return false if value1.is_a?(FalseClass)
|
22
|
+
return true if value1.is_a?(TrueClass)
|
23
|
+
|
24
|
+
return success ? value1 : false
|
25
|
+
end
|
26
|
+
|
27
|
+
# Handle the right-hand side, which might have many values and operators, e.g. '{x=a|b|c|d?}'
|
28
|
+
i = start
|
29
|
+
result = false
|
30
|
+
while i < condition.values.length
|
31
|
+
item = condition.values[i]
|
32
|
+
if item.primitive.constant
|
33
|
+
value2 = item.text
|
34
|
+
else
|
35
|
+
value2, success = value(global, item.text)
|
36
|
+
end
|
37
|
+
partial = false
|
38
|
+
case condition.operator
|
39
|
+
when '='
|
40
|
+
wild = value2.is_a?(String) && value2.include?('*') ? true : false
|
41
|
+
if wild
|
42
|
+
regex = '^'
|
43
|
+
value2.each_char do |c|
|
44
|
+
regex << (c == '*' ? '.*' : c)
|
45
|
+
end
|
46
|
+
partial |= !value1.match(regex).nil?
|
47
|
+
else
|
48
|
+
partial |= value1 == value2
|
49
|
+
end
|
50
|
+
when '>'
|
51
|
+
partial |= value1 > value2
|
52
|
+
when '<'
|
53
|
+
partial |= value1 < value2
|
54
|
+
when '>='
|
55
|
+
partial |= value1 >= value2
|
56
|
+
when '<='
|
57
|
+
partial |= value1 <= value2
|
58
|
+
end
|
59
|
+
i += 1
|
60
|
+
result |= partial
|
61
|
+
end
|
62
|
+
result
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.value(global, k)
|
66
|
+
success = false
|
67
|
+
if k.is_a?(String) && k.include?('%')
|
68
|
+
value1, _ignore = Modl::Parser::RefProcessor.deref(k, global)
|
69
|
+
success = true
|
70
|
+
elsif k.is_a?(FalseClass)
|
71
|
+
value1 = false
|
72
|
+
success = true
|
73
|
+
elsif k.is_a?(TrueClass)
|
74
|
+
value1 = true
|
75
|
+
success = true
|
76
|
+
elsif k.is_a?(NilClass)
|
77
|
+
value1 = nil
|
78
|
+
else
|
79
|
+
key = k
|
80
|
+
ikey = key.to_i
|
81
|
+
if ikey.to_s == key
|
82
|
+
index_val = global.index[ikey]
|
83
|
+
value1 = index_val.respond_to?(:text) ? index_val.text : nil
|
84
|
+
else
|
85
|
+
pair = global.pair(key)
|
86
|
+
return k unless pair
|
87
|
+
|
88
|
+
value1 = pair.text
|
89
|
+
end
|
90
|
+
success = true
|
91
|
+
end
|
92
|
+
[value1, success]
|
93
|
+
end
|
94
|
+
|
95
|
+
def evaluate_old
|
96
|
+
result = false
|
97
|
+
if @key
|
98
|
+
if @key.include?('%')
|
99
|
+
value1, _ignore = Modl::Parser::RefProcessor.deref(@key, @global)
|
100
|
+
else
|
101
|
+
key = @key
|
102
|
+
ikey = key.to_i
|
103
|
+
if ikey.to_s == key
|
104
|
+
index_val = @global.index[ikey]
|
105
|
+
value1 = index_val.respond_to?(:text) ? index_val.text : nil
|
106
|
+
else
|
107
|
+
pair = @global.pair(key)
|
108
|
+
return false unless pair
|
109
|
+
|
110
|
+
value1 = pair.text
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
@values.each do |value|
|
115
|
+
value2 = value.text
|
116
|
+
value2, _ignore = Modl::Parser::RefProcessor.deref(value2, @global) if value2.is_a?(String) && value2.include?('%')
|
117
|
+
value2 = @global.pair(value.text).text if @global.pair(value.text)
|
118
|
+
|
119
|
+
case @operator
|
120
|
+
when '='
|
121
|
+
wild = value2.is_a?(String) && value2.include?('*') ? true : false
|
122
|
+
if wild
|
123
|
+
regex = '^'
|
124
|
+
value2.each_char do |c|
|
125
|
+
regex << (c == '*' ? '.*' : c)
|
126
|
+
end
|
127
|
+
result |= !value1.match(regex).nil?
|
128
|
+
else
|
129
|
+
result |= value1 == value2
|
130
|
+
end
|
131
|
+
when '>'
|
132
|
+
result |= value1 > value2
|
133
|
+
when '<'
|
134
|
+
result |= value1 < value2
|
135
|
+
when '>='
|
136
|
+
result |= value1 >= value2
|
137
|
+
when '<='
|
138
|
+
result |= value1 <= value2
|
139
|
+
end
|
140
|
+
break if result # shortcut if we have a matching value
|
141
|
+
end
|
142
|
+
elsif @values.length == 1
|
143
|
+
key = @values[0].text
|
144
|
+
if key.is_a?(String)
|
145
|
+
key = key.start_with?('%') ? Sutil.tail(key) : key
|
146
|
+
end
|
147
|
+
the_pair = @global.pair(key)
|
148
|
+
if the_pair
|
149
|
+
result = the_pair.text
|
150
|
+
else
|
151
|
+
return true if @values[0].trueVal
|
152
|
+
return false if @values[0].falseVal
|
153
|
+
return false if @values[0].string
|
154
|
+
|
155
|
+
result = @values[0].evaluate
|
156
|
+
end
|
157
|
+
end
|
158
|
+
result
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'modl/parser/object_cache'
|
2
|
+
require 'modl/parser/sutil'
|
3
|
+
|
4
|
+
module Modl
|
5
|
+
module Parser
|
6
|
+
|
7
|
+
# This class handled file loading from local or remote file systems.
|
8
|
+
class FileImporter
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@cache = ObjectCache.new
|
12
|
+
end
|
13
|
+
|
14
|
+
# Supply a single file name as a string or an array of file names.
|
15
|
+
def import_files(files, global)
|
16
|
+
file_names = []
|
17
|
+
file_names += files if files.is_a? Array
|
18
|
+
file_names << files if files.is_a? String
|
19
|
+
|
20
|
+
file_names.each do |file_name|
|
21
|
+
force = file_name.end_with?('!')
|
22
|
+
if force
|
23
|
+
# Don't use the cache if we're forcing a reload.
|
24
|
+
@cache.evict(file_name)
|
25
|
+
file_name = Sutil.head(file_name)
|
26
|
+
parsed = nil
|
27
|
+
else
|
28
|
+
# Do we have a cached version?
|
29
|
+
parsed = @cache.get(file_name)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Did we hit the cache?
|
33
|
+
unless parsed
|
34
|
+
# No.
|
35
|
+
file_name << '.modl' unless file_name.end_with?('.txt', '.modl')
|
36
|
+
file_name, new_val = RefProcessor.deref file_name, global if file_name.include?('%')
|
37
|
+
|
38
|
+
begin
|
39
|
+
uri = URI(file_name)
|
40
|
+
txt = Net::HTTP.get(uri)
|
41
|
+
rescue
|
42
|
+
begin
|
43
|
+
txt = File.readlines(file_name).join
|
44
|
+
rescue
|
45
|
+
raise InterpreterError, 'File not found: ' + file_name
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
global.loaded_file(file_name)
|
50
|
+
|
51
|
+
# Parse the downloaded file ands extract the classes
|
52
|
+
parsed = Modl::Parser::Parser.parse txt, global
|
53
|
+
# Save it for next time
|
54
|
+
@cache.put(file_name, parsed)
|
55
|
+
end
|
56
|
+
# Extract the JSON content and add the classes and pairs to the existing GlobalParseContext hashes.
|
57
|
+
parsed.extract_hash
|
58
|
+
global.merge_classes(parsed.global)
|
59
|
+
global.merge_pairs(parsed.global)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,249 @@
|
|
1
|
+
module Modl
|
2
|
+
module Parser
|
3
|
+
# Each time we run the parser on a MODL file we need to keep track of a few things so they can be made available
|
4
|
+
# to other areas of the code. This class is the container for that contextual information, which is gathered
|
5
|
+
# as the Modl::Parser:Parsed object processes the parse tree.
|
6
|
+
class GlobalParseContext
|
7
|
+
|
8
|
+
attr_accessor :syntax_version
|
9
|
+
attr_reader :interpreter_syntax_version
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
# Holds the index array from a MODL file, e.g. '?=a:b:c:d'
|
13
|
+
@index = []
|
14
|
+
# Contains all pairs as they are encountered in the parsing process.
|
15
|
+
@pairs = {}
|
16
|
+
# Contains all defined and loaded classes for the current MODL document.
|
17
|
+
@classes_by_id = {}
|
18
|
+
@classes_by_name = {}
|
19
|
+
# Hold the user-defined methods.
|
20
|
+
@methods_hash = {}
|
21
|
+
@methods_by_id = {}
|
22
|
+
# Tracks the nesting depth for conditional clauses.
|
23
|
+
@conditional = 0
|
24
|
+
# Defaults to 1 and can be overridden by the *version command.
|
25
|
+
@syntax_version = 1
|
26
|
+
@interpreter_syntax_version = 1
|
27
|
+
@loaded_files = []
|
28
|
+
end
|
29
|
+
|
30
|
+
def loaded_file(str)
|
31
|
+
@loaded_files << str unless str.nil?
|
32
|
+
end
|
33
|
+
|
34
|
+
def index_value(n, default)
|
35
|
+
return default if n > @index.length
|
36
|
+
|
37
|
+
@index[n]
|
38
|
+
end
|
39
|
+
|
40
|
+
def in_condition?
|
41
|
+
@conditional.positive?
|
42
|
+
end
|
43
|
+
|
44
|
+
def enter_condition
|
45
|
+
@conditional += 1
|
46
|
+
end
|
47
|
+
|
48
|
+
def exit_condition
|
49
|
+
@conditional -= 1
|
50
|
+
end
|
51
|
+
|
52
|
+
def pair(key, val = nil)
|
53
|
+
return @pairs[key] unless val
|
54
|
+
|
55
|
+
@pairs[key] = val
|
56
|
+
end
|
57
|
+
|
58
|
+
def classs(key)
|
59
|
+
if key.is_a? String
|
60
|
+
result = @classes_by_id[key]
|
61
|
+
result = @classes_by_name[key] if result.nil?
|
62
|
+
result
|
63
|
+
elsif key.is_a? MODLClass
|
64
|
+
@classes_by_id[key.id] = key if key.id
|
65
|
+
@classes_by_name[key.name] = key if key.name
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def merge_pairs(other)
|
70
|
+
@pairs.merge!(other.all_pairs)
|
71
|
+
end
|
72
|
+
|
73
|
+
def merge_classes(other)
|
74
|
+
@classes_by_id.merge!(other.all_classes_by_id)
|
75
|
+
@classes_by_name.merge!(other.all_classes_by_name)
|
76
|
+
end
|
77
|
+
|
78
|
+
def has_pairs?
|
79
|
+
@pairs.length.positive?
|
80
|
+
end
|
81
|
+
|
82
|
+
def has_class?(key)
|
83
|
+
@classes_by_id.keys.include?(key) || @classes_by_name.keys.include?(key)
|
84
|
+
end
|
85
|
+
|
86
|
+
def has_user_method?(key)
|
87
|
+
@methods_hash.keys.include?(key)
|
88
|
+
end
|
89
|
+
|
90
|
+
def add_to_index(item)
|
91
|
+
@index << item
|
92
|
+
end
|
93
|
+
|
94
|
+
def user_method(key, val = nil)
|
95
|
+
return @methods_hash[key] unless val
|
96
|
+
|
97
|
+
@methods_hash[key] = val
|
98
|
+
end
|
99
|
+
|
100
|
+
def user_method_id(key, val)
|
101
|
+
@methods_hash[key] = val
|
102
|
+
@methods_by_id[key] = val
|
103
|
+
end
|
104
|
+
|
105
|
+
def class_list
|
106
|
+
result = []
|
107
|
+
@classes_by_id.values.each do |clazz|
|
108
|
+
new_item = {}
|
109
|
+
new_item[clazz.id] = class_to_hash(clazz)
|
110
|
+
result << new_item
|
111
|
+
end
|
112
|
+
result
|
113
|
+
end
|
114
|
+
|
115
|
+
def method_list
|
116
|
+
result = []
|
117
|
+
@methods_by_id.values.each do |m|
|
118
|
+
new_item = {}
|
119
|
+
new_item[m.id] = method_to_hash(m)
|
120
|
+
result << new_item
|
121
|
+
end
|
122
|
+
result
|
123
|
+
end
|
124
|
+
|
125
|
+
def file_list
|
126
|
+
@loaded_files.dup
|
127
|
+
end
|
128
|
+
|
129
|
+
def id_list
|
130
|
+
ids = {}
|
131
|
+
ids['methods'] = @methods_by_id.keys.dup.sort!
|
132
|
+
ids['classes'] = @classes_by_id.keys.dup.sort!
|
133
|
+
ids
|
134
|
+
end
|
135
|
+
|
136
|
+
def name_list
|
137
|
+
names = {}
|
138
|
+
names['methods'] = @methods_hash.keys.dup.sort!
|
139
|
+
names['classes'] = @classes_by_name.keys.dup.sort!
|
140
|
+
names
|
141
|
+
end
|
142
|
+
|
143
|
+
def superclass_list
|
144
|
+
result = {}
|
145
|
+
@classes_by_id.each do |c|
|
146
|
+
result[c[0]] = c[1].superclass
|
147
|
+
end
|
148
|
+
result
|
149
|
+
end
|
150
|
+
|
151
|
+
def assign_list
|
152
|
+
result = {}
|
153
|
+
@classes_by_id.each do |c|
|
154
|
+
result[c[0]] = c[1].assign
|
155
|
+
end
|
156
|
+
result
|
157
|
+
end
|
158
|
+
|
159
|
+
def transform_list
|
160
|
+
result = {}
|
161
|
+
@methods_by_id.each do |c|
|
162
|
+
result[c[0]] = c[1].transform
|
163
|
+
end
|
164
|
+
result
|
165
|
+
end
|
166
|
+
|
167
|
+
def allow_list
|
168
|
+
result = {}
|
169
|
+
@classes_by_id.each do |c|
|
170
|
+
result[c[0]] = c[1].allow.nil? ? nil : c[1].allow.extract_hash
|
171
|
+
end
|
172
|
+
result
|
173
|
+
end
|
174
|
+
|
175
|
+
protected
|
176
|
+
|
177
|
+
def all_classes_by_id
|
178
|
+
@classes_by_id
|
179
|
+
end
|
180
|
+
|
181
|
+
def all_classes_by_name
|
182
|
+
@classes_by_name
|
183
|
+
end
|
184
|
+
|
185
|
+
def all_pairs
|
186
|
+
@pairs
|
187
|
+
end
|
188
|
+
|
189
|
+
private
|
190
|
+
|
191
|
+
def class_to_hash(clazz)
|
192
|
+
map = {}
|
193
|
+
# name
|
194
|
+
map['name'] = clazz.name
|
195
|
+
|
196
|
+
# superclass
|
197
|
+
map['superclass'] = clazz.superclass
|
198
|
+
|
199
|
+
# assign
|
200
|
+
if clazz.assign
|
201
|
+
map['assign'] = clazz.assign
|
202
|
+
end
|
203
|
+
|
204
|
+
# allow
|
205
|
+
if clazz.allow
|
206
|
+
map['allow'] = clazz.allow.extract_hash
|
207
|
+
end
|
208
|
+
|
209
|
+
# content
|
210
|
+
if clazz.content.length.positive?
|
211
|
+
clazz.content.each do |item|
|
212
|
+
map[item[0]] = item[1].extract_hash
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
map
|
217
|
+
end
|
218
|
+
|
219
|
+
def method_to_hash(mthd)
|
220
|
+
map = {}
|
221
|
+
# name
|
222
|
+
map['name'] = mthd.name
|
223
|
+
|
224
|
+
# superclass
|
225
|
+
map['transform'] = mthd.transform
|
226
|
+
|
227
|
+
map
|
228
|
+
end
|
229
|
+
|
230
|
+
def new_map_item(key, value)
|
231
|
+
map_item = Parsed::ParsedMapItem.new(self)
|
232
|
+
map_item.pair = Parsed::ParsedPair.new(self)
|
233
|
+
map_item.pair.key = key
|
234
|
+
map_item.pair.valueItem = Parsed::ParsedValueItem.new(self)
|
235
|
+
if value.is_a? Parsed::ParsedValue
|
236
|
+
map_item.pair.valueItem.value = value
|
237
|
+
else
|
238
|
+
map_item.pair.valueItem.value = Parsed::ParsedValue.new(self)
|
239
|
+
map_item.pair.valueItem.value.primitive = Parsed::ParsedPrimitive.new(self)
|
240
|
+
map_item.pair.valueItem.value.primitive = Parsed::ParsedPrimitive.new(self)
|
241
|
+
map_item.pair.valueItem.value.primitive.string = Parsed::ParsedString.new(value)
|
242
|
+
map_item.pair.valueItem.value.primitive.text = value
|
243
|
+
map_item.pair.valueItem.value.text = value
|
244
|
+
end
|
245
|
+
map_item
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Modl
|
2
|
+
module Parser
|
3
|
+
# This class handles the conversion of objects that refer to classes into instances of those classes.
|
4
|
+
# It works recursively since class usage can be nested.
|
5
|
+
class InstructionProcessor
|
6
|
+
def self.process(global, obj)
|
7
|
+
if obj.is_a? Hash
|
8
|
+
nvals = {}
|
9
|
+
obj.keys.each do |k|
|
10
|
+
o = obj[k]
|
11
|
+
if o.is_a? String
|
12
|
+
nv = process_instruction(global, o)
|
13
|
+
nvals[k] = nv unless nv.nil?
|
14
|
+
else
|
15
|
+
process(global, o)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
obj.merge!(nvals)
|
19
|
+
elsif obj.is_a? Array
|
20
|
+
i = 0
|
21
|
+
while i < obj.length
|
22
|
+
o = obj[i]
|
23
|
+
if o.is_a? String
|
24
|
+
nv = process_instruction(global, o)
|
25
|
+
obj[i] = nv unless nv.nil?
|
26
|
+
else
|
27
|
+
process(global, o)
|
28
|
+
end
|
29
|
+
i += 1
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.process_instruction(global, str)
|
35
|
+
case str
|
36
|
+
when '%*class'
|
37
|
+
return global.class_list
|
38
|
+
when '%*method'
|
39
|
+
return global.method_list
|
40
|
+
when '%*load'
|
41
|
+
return global.file_list
|
42
|
+
when '%*id'
|
43
|
+
return global.id_list
|
44
|
+
when '%*name'
|
45
|
+
return global.name_list
|
46
|
+
when '%*superclass'
|
47
|
+
return global.superclass_list
|
48
|
+
when '%*assign'
|
49
|
+
return global.assign_list
|
50
|
+
when '%*transform'
|
51
|
+
return global.transform_list
|
52
|
+
when '%*allow'
|
53
|
+
return global.allow_list
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'modl/parser/MODLParserListener'
|
2
|
+
require 'modl/parser/MODLParserVisitor'
|
3
|
+
require 'modl/parser/MODLLexer'
|
4
|
+
require 'modl/parser/MODLParser'
|
5
|
+
require 'modl/parser/class_processor'
|
6
|
+
require 'modl/parser/parser'
|
7
|
+
require 'json'
|
8
|
+
|
9
|
+
module Modl
|
10
|
+
# Interpreter-specific errors
|
11
|
+
class InterpreterError < StandardError
|
12
|
+
end
|
13
|
+
|
14
|
+
# This is the main Ruby Interpreter entry point. Supply a String containing MODL text and it will return a String
|
15
|
+
# containing the JSON equivalent. The JSON isn't pretty-printed unless pretty is true
|
16
|
+
class Interpreter
|
17
|
+
def self.interpret(str, pretty = false)
|
18
|
+
# Parse the MODL string into a Modl::Parser::Parsed object.
|
19
|
+
parsed = Modl::Parser::Parser.parse str
|
20
|
+
|
21
|
+
# Convert the Parsed object into a simpler structure of and Array or Hash
|
22
|
+
interpreted = parsed.extract_hash
|
23
|
+
|
24
|
+
# Process any class definitions used by the MODL file.
|
25
|
+
Modl::Parser::ClassProcessor.process(parsed.global, interpreted)
|
26
|
+
Modl::Parser::InstructionProcessor.process(parsed.global, interpreted)
|
27
|
+
# If the result is a simple string then just return it.
|
28
|
+
return interpreted if interpreted.is_a? String
|
29
|
+
|
30
|
+
# Otherwise generate a JSON string.
|
31
|
+
if pretty
|
32
|
+
JSON.pretty_generate interpreted
|
33
|
+
else
|
34
|
+
JSON.generate interpreted
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module Modl
|
2
|
+
module Parser
|
3
|
+
# Represents a *class defined, or loaded by, a MODL document.
|
4
|
+
class MODLClass
|
5
|
+
attr_accessor :id
|
6
|
+
attr_accessor :name
|
7
|
+
attr_accessor :superclass
|
8
|
+
attr_accessor :assign
|
9
|
+
attr_accessor :content
|
10
|
+
attr_accessor :allow
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@content = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
# Find a keylist of the right length from the *assign array of arrays.
|
17
|
+
def keylist_of_length(len)
|
18
|
+
return [] if @assign.nil?
|
19
|
+
|
20
|
+
@assign.each do |kl|
|
21
|
+
return kl if kl.length == len
|
22
|
+
end
|
23
|
+
raise InterpreterError,
|
24
|
+
'No key list of the correct length in class ' + @id + ' - looking for one of length ' + len.to_s
|
25
|
+
end
|
26
|
+
|
27
|
+
def merge_content(new_value)
|
28
|
+
@content.each do |k, v|
|
29
|
+
new_value[k] = v.extract_hash
|
30
|
+
end
|
31
|
+
new_value
|
32
|
+
end
|
33
|
+
|
34
|
+
def name_or_id
|
35
|
+
@name.nil? ? @id : @name
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Extract a class from a ParsedPair object
|
40
|
+
class ClassExtractor
|
41
|
+
def self.extract(pair, global)
|
42
|
+
return unless pair.type == 'class'
|
43
|
+
|
44
|
+
clazz = MODLClass.new
|
45
|
+
map = pair.map if pair.map
|
46
|
+
map = pair.valueItem&.value&.map if pair.valueItem&.value&.map
|
47
|
+
|
48
|
+
map.mapItems.each do |item|
|
49
|
+
next unless item&.pair&.type
|
50
|
+
|
51
|
+
case item&.pair&.type
|
52
|
+
when 'id'
|
53
|
+
str_value = item.pair.valueItem.value.primitive.string.string
|
54
|
+
raise InterpreterError, 'Reserved class id - cannot redefine: ' + str_value if reserved?(str_value)
|
55
|
+
|
56
|
+
clazz.id = str_value
|
57
|
+
when 'name'
|
58
|
+
str_value = item.pair.valueItem.value.primitive.string.string
|
59
|
+
raise InterpreterError, 'Reserved class name - cannot redefine: ' + str_value if reserved?(str_value)
|
60
|
+
|
61
|
+
clazz.name = str_value
|
62
|
+
when 'superclass'
|
63
|
+
str_value = item.pair.valueItem.value.primitive.string.string
|
64
|
+
clazz.superclass = str_value
|
65
|
+
when 'keylist'
|
66
|
+
clazz.assign = item.pair.key_lists
|
67
|
+
when 'allow'
|
68
|
+
clazz.allow = item.pair.array if item.pair.array
|
69
|
+
clazz.allow = item.pair.valueItem.value.array if item.pair.valueItem.value.array
|
70
|
+
else
|
71
|
+
clazz.content[item.pair.key] = item.pair.array if item.pair.array
|
72
|
+
clazz.content[item.pair.key] = item.pair.map if item.pair.map
|
73
|
+
clazz.content[item.pair.key] = item.pair.valueItem.value if item.pair.valueItem.value
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
superclass = clazz.superclass
|
78
|
+
|
79
|
+
if superclass && !reserved?(superclass) && !global.has_class?(superclass)
|
80
|
+
raise InterpreterError, 'Invalid superclass: ' + superclass.to_s
|
81
|
+
end
|
82
|
+
raise InterpreterError, 'Missing id for class' if clazz.id.nil?
|
83
|
+
|
84
|
+
# Make sure the class name isn't redefining an existing class
|
85
|
+
if !global.has_class?(clazz.id) && !global.has_class?(clazz.name)
|
86
|
+
|
87
|
+
# store the classes by id and name to make them easier to find later
|
88
|
+
global.classs(clazz)
|
89
|
+
else
|
90
|
+
id = clazz.id.nil? ? 'undefined' : clazz.id
|
91
|
+
name = clazz.name.nil? ? 'undefined' : clazz.name
|
92
|
+
raise InterpreterError, 'Class name or id already defined - cannot redefine: ' + id + ', ' + name
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Check for a reserved class name
|
97
|
+
def self.reserved?(str)
|
98
|
+
%w[map str arr num].include?(str)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Modl
|
2
|
+
module Parser
|
3
|
+
# Extracts an index definition from a ParsedPair
|
4
|
+
class IndexExtractor
|
5
|
+
def self.extract(pair, global)
|
6
|
+
item = pair.valueItem if pair.valueItem
|
7
|
+
item = pair.array if pair.array
|
8
|
+
|
9
|
+
# collect all values from the object
|
10
|
+
if item.is_a? Parsed::ParsedValueItem
|
11
|
+
if item&.value&.text
|
12
|
+
global.index << item.value.text
|
13
|
+
elsif item&.value&.array
|
14
|
+
item.value.array.abstractArrayItems.each do |avi|
|
15
|
+
global.add_to_index(avi.arrayValueItem)
|
16
|
+
end
|
17
|
+
elsif item&.value&.nbArray
|
18
|
+
item.value.nbArray.arrayItems.each do |avi|
|
19
|
+
global.add_to_index(avi.arrayValueItem)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
elsif item.is_a? Parsed::ParsedArray
|
23
|
+
item.abstractArrayItems.each do |avi|
|
24
|
+
global.add_to_index(avi.arrayValueItem)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|