modl 0.0.2 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|