gloo 6.7.2 → 7.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 +4 -4
- data/CLAUDE.md +1 -0
- data/docs/application.md +3 -1
- data/docs/iterators.md +39 -3
- data/docs/language_syntax.md +53 -5
- data/docs/objects.md +58 -1
- data/docs/operators.md +16 -0
- data/docs/plugins.md +9 -1
- data/docs/verbs.md +4 -2
- data/lib/VERSION +1 -1
- data/lib/VERSION_NOTES +20 -0
- data/lib/gloo/app/engine.rb +64 -8
- data/lib/gloo/app/log.rb +47 -6
- data/lib/gloo/convert/converter.rb +63 -2
- data/lib/gloo/convert/string_to_date.rb +14 -0
- data/lib/gloo/convert/string_to_datetime.rb +16 -0
- data/lib/gloo/convert/string_to_decimal.rb +13 -0
- data/lib/gloo/convert/string_to_integer.rb +9 -2
- data/lib/gloo/convert/string_to_time.rb +28 -0
- data/lib/gloo/core/error.rb +11 -2
- data/lib/gloo/core/event_manager.rb +7 -4
- data/lib/gloo/core/factory.rb +16 -11
- data/lib/gloo/core/here.rb +6 -1
- data/lib/gloo/core/invoker.rb +2 -3
- data/lib/gloo/core/not_found.rb +42 -0
- data/lib/gloo/core/obj.rb +28 -2
- data/lib/gloo/core/op.rb +2 -0
- data/lib/gloo/core/parser.rb +1 -1
- data/lib/gloo/core/pn.rb +33 -12
- data/lib/gloo/core/tokens.rb +38 -0
- data/lib/gloo/core/verb.rb +54 -0
- data/lib/gloo/exec/dispatch.rb +5 -6
- data/lib/gloo/exec/exec_env.rb +13 -0
- data/lib/gloo/exec/runner.rb +10 -2
- data/lib/gloo/exec/script.rb +13 -1
- data/lib/gloo/expr/expression.rb +39 -1
- data/lib/gloo/expr/op_plus.rb +3 -0
- data/lib/gloo/objs/basic/boolean.rb +16 -0
- data/lib/gloo/objs/basic/container.rb +154 -6
- data/lib/gloo/objs/basic/decimal.rb +1 -0
- data/lib/gloo/objs/basic/integer.rb +1 -0
- data/lib/gloo/objs/basic/string.rb +0 -1
- data/lib/gloo/objs/basic/string_msgs.rb +68 -32
- data/lib/gloo/objs/ctrl/each.rb +22 -3
- data/lib/gloo/objs/ctrl/each_dir.rb +20 -7
- data/lib/gloo/objs/ctrl/each_file.rb +78 -9
- data/lib/gloo/objs/ctrl/function.rb +6 -5
- data/lib/gloo/objs/dt/date.rb +28 -11
- data/lib/gloo/objs/dt/datetime.rb +72 -21
- data/lib/gloo/objs/dt/dt_tools.rb +46 -2
- data/lib/gloo/objs/dt/time.rb +28 -11
- data/lib/gloo/objs/str_utils/cipher.rb +17 -4
- data/lib/gloo/objs/str_utils/password.rb +6 -2
- data/lib/gloo/objs/system/erb.rb +9 -3
- data/lib/gloo/objs/system/file_handle.rb +57 -21
- data/lib/gloo/objs/system/system.rb +6 -2
- data/lib/gloo/objs/web/http_get.rb +12 -2
- data/lib/gloo/objs/web/http_post.rb +6 -2
- data/lib/gloo/objs/web/json.rb +38 -18
- data/lib/gloo/objs/web/uri.rb +36 -16
- data/lib/gloo/persist/file_loader.rb +72 -11
- data/lib/gloo/persist/file_saver.rb +2 -1
- data/lib/gloo/persist/indent_stack.rb +49 -33
- data/lib/gloo/persist/line_splitter.rb +40 -3
- data/lib/gloo/persist/persist_man.rb +2 -4
- data/lib/gloo/plugin/ext_manager.rb +6 -9
- data/lib/gloo/plugin/lib_manager.rb +2 -2
- data/lib/gloo/verbs/break.rb +1 -0
- data/lib/gloo/verbs/check.rb +1 -1
- data/lib/gloo/verbs/cls.rb +1 -0
- data/lib/gloo/verbs/create.rb +32 -4
- data/lib/gloo/verbs/execute.rb +1 -1
- data/lib/gloo/verbs/exists.rb +27 -15
- data/lib/gloo/verbs/files.rb +2 -1
- data/lib/gloo/verbs/help.rb +1 -0
- data/lib/gloo/verbs/if.rb +1 -1
- data/lib/gloo/verbs/invoke.rb +1 -1
- data/lib/gloo/verbs/list.rb +4 -3
- data/lib/gloo/verbs/load.rb +3 -3
- data/lib/gloo/verbs/move.rb +10 -8
- data/lib/gloo/verbs/put.rb +20 -8
- data/lib/gloo/verbs/quit.rb +1 -0
- data/lib/gloo/verbs/redirect.rb +8 -2
- data/lib/gloo/verbs/reload.rb +9 -1
- data/lib/gloo/verbs/run.rb +2 -2
- data/lib/gloo/verbs/save.rb +4 -2
- data/lib/gloo/verbs/tell.rb +1 -1
- data/lib/gloo/verbs/unless.rb +1 -1
- data/lib/gloo/verbs/unload.rb +8 -0
- data/test.gloo/ctrl/each.test.gloo +88 -0
- data/test.gloo/lang/declaration.test.gloo +27 -0
- data/test.gloo/lang/exceptions.test.gloo +60 -0
- data/test.gloo/lang/failures.test.gloo +139 -0
- data/test.gloo/lang/it.test.gloo +133 -0
- data/test.gloo/lang/naming.test.gloo +11 -0
- data/test.gloo/lang/ops.test.gloo +30 -0
- data/test.gloo/objs/can.test.gloo +101 -0
- data/test.gloo/objs/string.test.gloo +24 -0
- data/test.gloo/string/str.test.gloo +28 -0
- data/test.gloo/verbs/create.test.gloo +36 -0
- data/test.gloo/verbs/exists.test.gloo +33 -1
- data/test.gloo/verbs/unload.test.gloo +34 -0
- metadata +4 -1
|
@@ -41,6 +41,7 @@ module Gloo
|
|
|
41
41
|
# Set the result of the system call.
|
|
42
42
|
#
|
|
43
43
|
def set_result( data )
|
|
44
|
+
@engine.heap.it.set_to data
|
|
44
45
|
r = find_child_resolve_alias RESULT
|
|
45
46
|
return unless r
|
|
46
47
|
|
|
@@ -110,7 +111,10 @@ module Gloo
|
|
|
110
111
|
cmd = cmd_value
|
|
111
112
|
return unless cmd
|
|
112
113
|
|
|
113
|
-
result =
|
|
114
|
+
result = nil
|
|
115
|
+
return false unless attempt( "run '#{cmd}'", SystemCallError ) do
|
|
116
|
+
result = `#{cmd}`
|
|
117
|
+
end
|
|
114
118
|
set_result result
|
|
115
119
|
end
|
|
116
120
|
|
|
@@ -143,7 +147,7 @@ module Gloo
|
|
|
143
147
|
'result (string) — The result of running the command. Only set if get_output is true.'
|
|
144
148
|
],
|
|
145
149
|
:messages => [
|
|
146
|
-
'run — Run the system level command. Sets the result value to the output of the command if get_output is true.'
|
|
150
|
+
'run — Run the system level command. Sets the result value to the output of the command if get_output is true, or to whether the command succeeded (true or false) if it is not. It will have the same value as result.'
|
|
147
151
|
],
|
|
148
152
|
:examples => <<~EXAMPLES.strip
|
|
149
153
|
date [system] :
|
|
@@ -14,6 +14,12 @@ module Gloo
|
|
|
14
14
|
|
|
15
15
|
KEYWORD = 'http_get'.freeze
|
|
16
16
|
KEYWORD_SHORT = 'get'.freeze
|
|
17
|
+
|
|
18
|
+
# What can go wrong reaching a URL: a bad URL, an unknown host,
|
|
19
|
+
# no connection, a timeout, an SSL problem, a bad response.
|
|
20
|
+
NETWORK_ERRORS = [ URI::InvalidURIError, SocketError, SystemCallError,
|
|
21
|
+
IOError, Timeout::Error, OpenSSL::SSL::SSLError,
|
|
22
|
+
Net::ProtocolError ].freeze
|
|
17
23
|
URL = 'uri'.freeze
|
|
18
24
|
DEFAULT_URL = 'https://web.site/'.freeze
|
|
19
25
|
PARAMS = 'params'.freeze
|
|
@@ -76,7 +82,10 @@ module Gloo
|
|
|
76
82
|
def msg_run
|
|
77
83
|
url = full_url_value
|
|
78
84
|
@engine.log.debug url
|
|
79
|
-
r =
|
|
85
|
+
r = nil
|
|
86
|
+
return false unless attempt( "get #{url}", *NETWORK_ERRORS ) do
|
|
87
|
+
r = Gloo::Objs::HttpGet.invoke_request( url, skip_ssl_verify? )
|
|
88
|
+
end
|
|
80
89
|
update_result r
|
|
81
90
|
end
|
|
82
91
|
|
|
@@ -125,6 +134,7 @@ module Gloo
|
|
|
125
134
|
# Set the result of the API call.
|
|
126
135
|
#
|
|
127
136
|
def update_result( data )
|
|
137
|
+
@engine.heap.it.set_to data
|
|
128
138
|
r = find_child_resolve_alias RESULT
|
|
129
139
|
return unless r
|
|
130
140
|
|
|
@@ -167,7 +177,7 @@ module Gloo
|
|
|
167
177
|
'skip_ssl_verify (boolean) — Optional. Skip the SSL verification as part of the request.'
|
|
168
178
|
],
|
|
169
179
|
:messages => [
|
|
170
|
-
'run — Run the HTTP Get and update the result.'
|
|
180
|
+
'run — Run the HTTP Get and update the result. It will have the response body.'
|
|
171
181
|
],
|
|
172
182
|
:examples => <<~EXAMPLES.strip
|
|
173
183
|
g [http_get] :
|
|
@@ -60,6 +60,7 @@ module Gloo
|
|
|
60
60
|
# Set the result of the API call.
|
|
61
61
|
#
|
|
62
62
|
def update_result( data )
|
|
63
|
+
@engine.heap.it.set_to data
|
|
63
64
|
r = find_child_resolve_alias RESULT
|
|
64
65
|
return unless r
|
|
65
66
|
|
|
@@ -111,7 +112,10 @@ module Gloo
|
|
|
111
112
|
@engine.log.debug "posting to: #{uri}"
|
|
112
113
|
body = self.body_as_json
|
|
113
114
|
@engine.log.debug "posting body: #{body}"
|
|
114
|
-
result =
|
|
115
|
+
result = nil
|
|
116
|
+
return false unless attempt( "post to #{uri}", *HttpGet::NETWORK_ERRORS ) do
|
|
117
|
+
result = Gloo::Objs::HttpPost.post_json( uri, body, skip_ssl_verify? )
|
|
118
|
+
end
|
|
115
119
|
@engine.log.debug result.code
|
|
116
120
|
@engine.log.debug result.message
|
|
117
121
|
|
|
@@ -171,7 +175,7 @@ module Gloo
|
|
|
171
175
|
'skip_ssl_verify (boolean) — Optional. Skip the SSL verification as part of the request.'
|
|
172
176
|
],
|
|
173
177
|
:messages => [
|
|
174
|
-
'run — Run the HTTP Post, sending the body data to the endpoint specified in the URI.'
|
|
178
|
+
'run — Run the HTTP Post, sending the body data to the endpoint specified in the URI. The result child (if there is one) gets the response body. It will have the response body.'
|
|
175
179
|
],
|
|
176
180
|
:examples => <<~EXAMPLES.strip
|
|
177
181
|
post [post] :
|
data/lib/gloo/objs/web/json.rb
CHANGED
|
@@ -65,12 +65,15 @@ module Gloo
|
|
|
65
65
|
def msg_pretty
|
|
66
66
|
return unless self.value
|
|
67
67
|
|
|
68
|
-
json =
|
|
69
|
-
|
|
70
|
-
json = JSON.parse(
|
|
68
|
+
json = nil
|
|
69
|
+
return false unless attempt( "parse the JSON in #{pn}", JSON::ParserError ) do
|
|
70
|
+
json = JSON.parse( self.value )
|
|
71
|
+
json = JSON.parse( json ) if self.value.start_with?( '"{' )
|
|
71
72
|
end
|
|
72
73
|
pretty = JSON.pretty_generate( json )
|
|
73
74
|
set_value pretty
|
|
75
|
+
@engine.heap.it.set_to pretty
|
|
76
|
+
return pretty
|
|
74
77
|
end
|
|
75
78
|
|
|
76
79
|
#
|
|
@@ -78,13 +81,23 @@ module Gloo
|
|
|
78
81
|
# The additional parameter is the path to the value.
|
|
79
82
|
#
|
|
80
83
|
def msg_get
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
unless @params&.token_count&.positive?
|
|
85
|
+
@engine.syntax_err 'Missing path to the value!'
|
|
86
|
+
@engine.heap.it.set_to false
|
|
87
|
+
return false
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
expr = Gloo::Expr::Expression.new( @engine, @params.tokens )
|
|
91
|
+
data = expr.evaluate
|
|
92
|
+
if data.nil?
|
|
93
|
+
@engine.heap.it.set_to false
|
|
94
|
+
return false
|
|
84
95
|
end
|
|
85
|
-
return unless data
|
|
86
96
|
|
|
87
|
-
field =
|
|
97
|
+
field = nil
|
|
98
|
+
return false unless attempt( "parse the JSON in #{pn}", JSON::ParserError ) do
|
|
99
|
+
field = Gloo::Objs::Json.get_value_in_json self.value, data
|
|
100
|
+
end
|
|
88
101
|
@engine.heap.it.set_to field
|
|
89
102
|
return field
|
|
90
103
|
end
|
|
@@ -97,12 +110,12 @@ module Gloo
|
|
|
97
110
|
if @params&.token_count&.positive?
|
|
98
111
|
pn = Gloo::Core::Pn.new( @engine, @params.tokens.first )
|
|
99
112
|
unless pn&.exists?
|
|
100
|
-
@engine.err
|
|
101
|
-
return
|
|
113
|
+
@engine.err Gloo::Core::NotFound.object( @params.tokens.first )
|
|
114
|
+
return @engine.heap.it.set_to( false )
|
|
102
115
|
end
|
|
103
116
|
else
|
|
104
|
-
@engine.
|
|
105
|
-
return
|
|
117
|
+
@engine.syntax_err 'Source path for objects is required'
|
|
118
|
+
return @engine.heap.it.set_to( false )
|
|
106
119
|
end
|
|
107
120
|
parent = pn.resolve
|
|
108
121
|
|
|
@@ -124,16 +137,19 @@ module Gloo
|
|
|
124
137
|
if @params&.token_count&.positive?
|
|
125
138
|
pn = Gloo::Core::Pn.new( @engine, @params.tokens.first )
|
|
126
139
|
unless pn&.exists?
|
|
127
|
-
@engine.err
|
|
140
|
+
@engine.err Gloo::Core::NotFound.object( @params.tokens.first )
|
|
128
141
|
return
|
|
129
142
|
end
|
|
130
143
|
else
|
|
131
|
-
@engine.
|
|
144
|
+
@engine.syntax_err 'Destination path for parsed objects is required'
|
|
132
145
|
return
|
|
133
146
|
end
|
|
134
147
|
parent = pn.resolve
|
|
135
148
|
|
|
136
|
-
json =
|
|
149
|
+
json = nil
|
|
150
|
+
return unless attempt( "parse the JSON in #{self.pn}", JSON::ParserError, set_it: false ) do
|
|
151
|
+
json = JSON.parse( self.value )
|
|
152
|
+
end
|
|
137
153
|
self.handle_json_to_obj( json, parent )
|
|
138
154
|
end
|
|
139
155
|
|
|
@@ -196,8 +212,12 @@ module Gloo
|
|
|
196
212
|
#
|
|
197
213
|
def self.get_value_in_json( json, path_to_value )
|
|
198
214
|
data = JSON.parse( json )
|
|
199
|
-
path_to_value.split( '.' ).each do |segment|
|
|
200
|
-
data = data
|
|
215
|
+
path_to_value.to_s.split( '.' ).each do |segment|
|
|
216
|
+
data = case data
|
|
217
|
+
when Hash then data[ segment ]
|
|
218
|
+
when Array then segment.match?( /\A\d+\z/ ) ? data[ segment.to_i ] : nil
|
|
219
|
+
end
|
|
220
|
+
return nil if data.nil?
|
|
201
221
|
end
|
|
202
222
|
return data
|
|
203
223
|
end
|
|
@@ -221,7 +241,7 @@ module Gloo
|
|
|
221
241
|
'get ({path}) — Get a value from the JSON data. Example: tell myjson to get (\'title\'). The parameter is the path in JSON to the value we want. The value is put into it.',
|
|
222
242
|
'set ({obj.path}) — Convert an object to an approximate JSON value. Example: tell myjson to set (my.object). The parameter is the path to the object used as the source. Note this is an approximate conversion — a gloo object can have both a simple value and be a container for child objects, which JSON can\'t represent the same way.',
|
|
223
243
|
'parse ({dst.path}) — Parse the JSON data and put values in the object specified by the parameter. Example: tell myjson to parse (path.to.dst).',
|
|
224
|
-
'pretty — Make the JSON format pretty.'
|
|
244
|
+
'pretty — Make the JSON format pretty. The value of the json is changed. It will have the pretty JSON.'
|
|
225
245
|
],
|
|
226
246
|
:examples => <<~EXAMPLES.strip
|
|
227
247
|
#
|
data/lib/gloo/objs/web/uri.rb
CHANGED
|
@@ -61,12 +61,17 @@ module Gloo
|
|
|
61
61
|
# Get the expiration date for the certificate.
|
|
62
62
|
#
|
|
63
63
|
def msg_get_cert_expires
|
|
64
|
-
|
|
64
|
+
if value.blank?
|
|
65
|
+
@engine.err 'There is no URL to get the certificate for!'
|
|
66
|
+
return @engine.heap.it.set_to( false )
|
|
67
|
+
end
|
|
65
68
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
o = nil
|
|
70
|
+
return false unless attempt( "get the certificate for #{value}", *HttpGet::NETWORK_ERRORS ) do
|
|
71
|
+
uri = URI( value )
|
|
72
|
+
response = Net::HTTP.start( uri.host, uri.port, :use_ssl => true )
|
|
73
|
+
o = response.peer_cert.not_after
|
|
74
|
+
end
|
|
70
75
|
|
|
71
76
|
@engine.heap.it.set_to o
|
|
72
77
|
return o
|
|
@@ -77,9 +82,12 @@ module Gloo
|
|
|
77
82
|
# in the URL. Might be used to scroll down in the page.
|
|
78
83
|
#
|
|
79
84
|
def msg_get_fragment
|
|
80
|
-
return
|
|
85
|
+
return @engine.heap.it.set_to( '' ) if value.blank?
|
|
81
86
|
|
|
82
|
-
o =
|
|
87
|
+
o = nil
|
|
88
|
+
return false unless attempt( "read the URL '#{value}'", URI::InvalidURIError ) do
|
|
89
|
+
o = URI( value ).fragment
|
|
90
|
+
end
|
|
83
91
|
@engine.heap.it.set_to o
|
|
84
92
|
return o
|
|
85
93
|
end
|
|
@@ -89,9 +97,12 @@ module Gloo
|
|
|
89
97
|
# Example: id=121
|
|
90
98
|
#
|
|
91
99
|
def msg_get_query
|
|
92
|
-
return
|
|
100
|
+
return @engine.heap.it.set_to( '' ) if value.blank?
|
|
93
101
|
|
|
94
|
-
o =
|
|
102
|
+
o = nil
|
|
103
|
+
return false unless attempt( "read the URL '#{value}'", URI::InvalidURIError ) do
|
|
104
|
+
o = URI( value ).query
|
|
105
|
+
end
|
|
95
106
|
@engine.heap.it.set_to o
|
|
96
107
|
return o
|
|
97
108
|
end
|
|
@@ -101,9 +112,12 @@ module Gloo
|
|
|
101
112
|
# Example: /posts
|
|
102
113
|
#
|
|
103
114
|
def msg_get_path
|
|
104
|
-
return
|
|
115
|
+
return @engine.heap.it.set_to( '' ) if value.blank?
|
|
105
116
|
|
|
106
|
-
o =
|
|
117
|
+
o = nil
|
|
118
|
+
return false unless attempt( "read the URL '#{value}'", URI::InvalidURIError ) do
|
|
119
|
+
o = URI( value ).path
|
|
120
|
+
end
|
|
107
121
|
@engine.heap.it.set_to o
|
|
108
122
|
return o
|
|
109
123
|
end
|
|
@@ -113,9 +127,12 @@ module Gloo
|
|
|
113
127
|
# Example: google.com
|
|
114
128
|
#
|
|
115
129
|
def msg_get_host
|
|
116
|
-
return
|
|
130
|
+
return @engine.heap.it.set_to( '' ) if value.blank?
|
|
117
131
|
|
|
118
|
-
o =
|
|
132
|
+
o = nil
|
|
133
|
+
return false unless attempt( "read the URL '#{value}'", URI::InvalidURIError ) do
|
|
134
|
+
o = URI( value ).host
|
|
135
|
+
end
|
|
119
136
|
@engine.heap.it.set_to o
|
|
120
137
|
return o
|
|
121
138
|
end
|
|
@@ -125,9 +142,12 @@ module Gloo
|
|
|
125
142
|
# Example: http
|
|
126
143
|
#
|
|
127
144
|
def msg_get_scheme
|
|
128
|
-
return
|
|
145
|
+
return @engine.heap.it.set_to( '' ) if value.blank?
|
|
129
146
|
|
|
130
|
-
o =
|
|
147
|
+
o = nil
|
|
148
|
+
return false unless attempt( "read the URL '#{value}'", URI::InvalidURIError ) do
|
|
149
|
+
o = URI( value ).scheme
|
|
150
|
+
end
|
|
131
151
|
@engine.heap.it.set_to o
|
|
132
152
|
return o
|
|
133
153
|
end
|
|
@@ -165,7 +185,7 @@ module Gloo
|
|
|
165
185
|
`xdg-open "#{url}"`
|
|
166
186
|
end
|
|
167
187
|
else
|
|
168
|
-
engine
|
|
188
|
+
engine&.err 'Opening a URL is not supported on this platform.'
|
|
169
189
|
end
|
|
170
190
|
end
|
|
171
191
|
|
|
@@ -22,6 +22,13 @@ module Gloo
|
|
|
22
22
|
BEGIN_BLOCK = 'BEGIN'.freeze
|
|
23
23
|
END_BLOCK = 'END'.freeze
|
|
24
24
|
SPACE_CNT = 2
|
|
25
|
+
UNNAMED = 'unnamed_'.freeze
|
|
26
|
+
NO_NAME_ERR = 'Object name is missing; using '.freeze
|
|
27
|
+
MISSING_BRACKET_ERR = "Missing ']' after type ".freeze
|
|
28
|
+
OVER_INDENTED_WARNING = 'Indented more than one level deeper than ' \
|
|
29
|
+
'the line above; nesting it under that line.'.freeze
|
|
30
|
+
MISALIGNED_WARNING = "Indentation doesn't line up with any line " \
|
|
31
|
+
'above; nesting it under the closest one.'.freeze
|
|
25
32
|
|
|
26
33
|
# A 'load lib {name}' (or 'load ext {name}') statement at the top
|
|
27
34
|
# of a file, before the first object declaration. It makes a core
|
|
@@ -64,7 +71,7 @@ module Gloo
|
|
|
64
71
|
#
|
|
65
72
|
def load
|
|
66
73
|
unless @mech.exist?( @pn )
|
|
67
|
-
@engine.err
|
|
74
|
+
@engine.err Gloo::Core::NotFound.file( @pn )
|
|
68
75
|
return
|
|
69
76
|
end
|
|
70
77
|
|
|
@@ -72,9 +79,17 @@ module Gloo
|
|
|
72
79
|
@indent_stack = IndentStack.new( @engine.heap.root, @source_doc )
|
|
73
80
|
f = @mech.read( @pn )
|
|
74
81
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
82
|
+
exec_env = @engine.exec_env
|
|
83
|
+
outer_location = exec_env.load_location
|
|
84
|
+
begin
|
|
85
|
+
each_joined_line( f ) do |line, number|
|
|
86
|
+
exec_env.load_location = "#{@pn}:#{number}"
|
|
87
|
+
dispatch_line( line )
|
|
88
|
+
end
|
|
89
|
+
finish
|
|
90
|
+
ensure
|
|
91
|
+
exec_env.load_location = outer_location
|
|
92
|
+
end
|
|
78
93
|
end
|
|
79
94
|
|
|
80
95
|
#
|
|
@@ -89,10 +104,27 @@ module Gloo
|
|
|
89
104
|
end
|
|
90
105
|
|
|
91
106
|
#
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
|
|
95
|
-
|
|
107
|
+
# Yield each line of the file with its line number (counting from
|
|
108
|
+
# 1). A line ending in a backslash continues on the next line:
|
|
109
|
+
# the two are joined, dropping the backslash and the line break,
|
|
110
|
+
# and the joined line has the number of the line it starts on.
|
|
111
|
+
#
|
|
112
|
+
def each_joined_line( data )
|
|
113
|
+
pending = nil
|
|
114
|
+
start = 0
|
|
115
|
+
data.each_line.with_index( 1 ) do |line, number|
|
|
116
|
+
if pending
|
|
117
|
+
pending << line
|
|
118
|
+
else
|
|
119
|
+
pending = line.dup
|
|
120
|
+
start = number
|
|
121
|
+
end
|
|
122
|
+
next if pending.delete_suffix!( "\\\n" )
|
|
123
|
+
|
|
124
|
+
yield pending, start
|
|
125
|
+
pending = nil
|
|
126
|
+
end
|
|
127
|
+
yield pending, start if pending
|
|
96
128
|
end
|
|
97
129
|
|
|
98
130
|
# ---------------------------------------------------------------------
|
|
@@ -207,16 +239,44 @@ module Gloo
|
|
|
207
239
|
#
|
|
208
240
|
def finalize_declaration( line )
|
|
209
241
|
line_tabs = tab_count( line )
|
|
210
|
-
@indent_stack.place( line_tabs, @last, @last_node )
|
|
242
|
+
placed = @indent_stack.place( line_tabs, @last, @last_node )
|
|
243
|
+
warn_indent( placed ) if placed
|
|
211
244
|
create_declared_obj( line, line_tabs )
|
|
212
245
|
end
|
|
213
246
|
|
|
247
|
+
#
|
|
248
|
+
# A declaration with no name is a syntax error. The best guess is
|
|
249
|
+
# a placeholder name that isn't taken under the parent, so the
|
|
250
|
+
# object (its type, value and children) still loads.
|
|
251
|
+
#
|
|
252
|
+
def placeholder_name( parent )
|
|
253
|
+
n = 1
|
|
254
|
+
n += 1 while parent&.contains_child?( "#{UNNAMED}#{n}" )
|
|
255
|
+
name = "#{UNNAMED}#{n}"
|
|
256
|
+
@engine.syntax_err "#{NO_NAME_ERR}'#{name}'."
|
|
257
|
+
return name
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
#
|
|
261
|
+
# Warn about indentation that was understood, but probably isn't
|
|
262
|
+
# what was meant.
|
|
263
|
+
#
|
|
264
|
+
def warn_indent( placed )
|
|
265
|
+
msg = if placed == IndentStack::OVER_INDENTED
|
|
266
|
+
OVER_INDENTED_WARNING
|
|
267
|
+
else
|
|
268
|
+
MISALIGNED_WARNING
|
|
269
|
+
end
|
|
270
|
+
@engine.warn msg
|
|
271
|
+
end
|
|
272
|
+
|
|
214
273
|
#
|
|
215
274
|
# Create the heap object and its source node for one declaration
|
|
216
275
|
# line, and start body collection if its type calls for one.
|
|
217
276
|
#
|
|
218
277
|
def create_declared_obj( line, line_tabs )
|
|
219
278
|
name, type, value, style = split_declaration( line )
|
|
279
|
+
name ||= placeholder_name( @indent_stack.parent )
|
|
220
280
|
leaf, parent, roots, created = @shorthand.expand( name, @indent_stack.parent )
|
|
221
281
|
roots.each { |r| @ledger.root( r ) }
|
|
222
282
|
created.each { |c| @ledger.created( c ) }
|
|
@@ -234,8 +294,8 @@ module Gloo
|
|
|
234
294
|
# name re-declared across files -- @last is the same object
|
|
235
295
|
# across re-declarations (the factory returns the existing
|
|
236
296
|
# one), so a later, comment-less re-declaration doesn't blank
|
|
237
|
-
# out an earlier file's doc. @last can be nil for
|
|
238
|
-
#
|
|
297
|
+
# out an earlier file's doc. @last can be nil for a type that
|
|
298
|
+
# can't be created (an unknown type is created untyped).
|
|
239
299
|
@last.doc = node.doc if @last && @last.doc.to_s.strip.empty?
|
|
240
300
|
@indent_stack.node.children << node
|
|
241
301
|
@last_node = node
|
|
@@ -258,6 +318,7 @@ module Gloo
|
|
|
258
318
|
def split_declaration( line )
|
|
259
319
|
splitter = LineSplitter.new( line, @indent_stack&.tabs || 0 )
|
|
260
320
|
name, type, value = splitter.split
|
|
321
|
+
@engine.syntax_err "#{MISSING_BRACKET_ERR}'#{type}'." if splitter.missing_bracket
|
|
261
322
|
style = { :raw_tail => splitter.raw_tail, :block_style => :inline }
|
|
262
323
|
return name, type, value, style if @block_value == ''
|
|
263
324
|
|
|
@@ -263,7 +263,8 @@ module Gloo
|
|
|
263
263
|
return body_value_unchanged?( node ) if node.block_style == :body
|
|
264
264
|
|
|
265
265
|
trial = node.obj.class.new( @engine )
|
|
266
|
-
|
|
266
|
+
# Only a comparison: any bad-value warning was given on load.
|
|
267
|
+
@engine.without_warnings { trial.set_value( node.raw_value ) }
|
|
267
268
|
return trial.value == node.obj.value
|
|
268
269
|
rescue
|
|
269
270
|
return false
|
|
@@ -11,17 +11,27 @@ module Gloo
|
|
|
11
11
|
module Persist
|
|
12
12
|
class IndentStack
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
# What place reports about a line's indentation, when it's
|
|
15
|
+
# probably not what was meant (the loader warns about it).
|
|
16
|
+
OVER_INDENTED = :over_indented
|
|
17
|
+
MISALIGNED = :misaligned
|
|
15
18
|
|
|
16
19
|
#
|
|
17
20
|
# Set up a stack rooted at the given heap object and source node.
|
|
18
21
|
#
|
|
19
22
|
def initialize( root_obj, root_node )
|
|
20
|
-
@
|
|
23
|
+
@levels = [ 0 ]
|
|
21
24
|
@parent_stack = [ root_obj ]
|
|
22
25
|
@node_stack = [ root_node ]
|
|
23
26
|
end
|
|
24
27
|
|
|
28
|
+
#
|
|
29
|
+
# The indentation (in tabs) of the current nesting level.
|
|
30
|
+
#
|
|
31
|
+
def tabs
|
|
32
|
+
return @levels.last
|
|
33
|
+
end
|
|
34
|
+
|
|
25
35
|
#
|
|
26
36
|
# The heap object new children should be created under.
|
|
27
37
|
#
|
|
@@ -43,46 +53,52 @@ module Gloo
|
|
|
43
53
|
# new parent. last_obj/last_node are whatever was created for the
|
|
44
54
|
# previous declaration line.
|
|
45
55
|
#
|
|
56
|
+
# Each open nesting level remembers its own indentation, so a line
|
|
57
|
+
# nests under the line above whenever it's deeper (by any amount),
|
|
58
|
+
# and an outdent goes back to exactly the level it lines up with.
|
|
59
|
+
#
|
|
60
|
+
# Returns nil, or OVER_INDENTED if the line is more than one level
|
|
61
|
+
# deeper than the line above, or MISALIGNED if an outdent doesn't
|
|
62
|
+
# line up with any open level (it's nested under the closest one).
|
|
63
|
+
#
|
|
46
64
|
def place( line_tabs, last_obj, last_node )
|
|
47
|
-
indent
|
|
48
|
-
if indent.positive?
|
|
49
|
-
# last_obj is nil when the previous line failed to make an
|
|
50
|
-
# object (eg. an unknown type in a malformed file) -- keep
|
|
51
|
-
# the current parent rather than pushing nil.
|
|
52
|
-
@parent_stack.push( last_obj || @parent_stack.last )
|
|
53
|
-
@node_stack.push( last_node || @node_stack.last )
|
|
54
|
-
elsif indent.negative?
|
|
55
|
-
indent.abs.times do
|
|
56
|
-
# never pop the root -- a file with erratic (mixed
|
|
57
|
-
# tab/space) indentation can outdent further than it ever
|
|
58
|
-
# indented; clamp instead of emptying the stack.
|
|
59
|
-
break if @parent_stack.length <= 1
|
|
65
|
+
return indent( line_tabs, last_obj, last_node ) if line_tabs > tabs
|
|
60
66
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
67
|
+
popped = nil
|
|
68
|
+
while tabs > line_tabs
|
|
69
|
+
@levels.pop
|
|
70
|
+
popped = [ @parent_stack.pop, @node_stack.pop ]
|
|
64
71
|
end
|
|
72
|
+
return nil if tabs == line_tabs
|
|
73
|
+
|
|
74
|
+
# Between two open levels: best guess, keep it under the
|
|
75
|
+
# closest (deepest) one it's still inside of.
|
|
76
|
+
push_level( line_tabs, *popped )
|
|
77
|
+
return MISALIGNED
|
|
65
78
|
end
|
|
66
79
|
|
|
67
80
|
private
|
|
68
81
|
|
|
69
82
|
#
|
|
70
|
-
#
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
@tabs -= diff
|
|
82
|
-
return -diff
|
|
83
|
-
end
|
|
83
|
+
# A deeper line: the previous line's object/node becomes the
|
|
84
|
+
# new parent.
|
|
85
|
+
#
|
|
86
|
+
def indent( line_tabs, last_obj, last_node )
|
|
87
|
+
over = line_tabs > tabs + 1
|
|
88
|
+
# last_obj is nil when the previous line failed to make an
|
|
89
|
+
# object (eg. a type that can't be created) -- keep the
|
|
90
|
+
# current parent rather than pushing nil.
|
|
91
|
+
push_level( line_tabs, last_obj || parent, last_node || node )
|
|
92
|
+
return over ? OVER_INDENTED : nil
|
|
93
|
+
end
|
|
84
94
|
|
|
85
|
-
|
|
95
|
+
#
|
|
96
|
+
# Open a nesting level at the given indentation.
|
|
97
|
+
#
|
|
98
|
+
def push_level( line_tabs, obj, node )
|
|
99
|
+
@levels.push line_tabs
|
|
100
|
+
@parent_stack.push obj
|
|
101
|
+
@node_stack.push node
|
|
86
102
|
end
|
|
87
103
|
|
|
88
104
|
end
|
|
@@ -12,7 +12,7 @@ module Gloo
|
|
|
12
12
|
BEGIN_BLOCK = 'BEGIN'.freeze
|
|
13
13
|
END_BLOCK = 'END'.freeze
|
|
14
14
|
|
|
15
|
-
attr_reader :obj, :raw_tail
|
|
15
|
+
attr_reader :obj, :raw_tail, :missing_bracket
|
|
16
16
|
|
|
17
17
|
#
|
|
18
18
|
# Set up a line splitter
|
|
@@ -21,6 +21,7 @@ module Gloo
|
|
|
21
21
|
@line = line
|
|
22
22
|
@tabs = tabs
|
|
23
23
|
@raw_tail = ''
|
|
24
|
+
@missing_bracket = false
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
#
|
|
@@ -43,8 +44,19 @@ module Gloo
|
|
|
43
44
|
# a string value, which can matter when the value is used to
|
|
44
45
|
# build HTML.
|
|
45
46
|
#
|
|
47
|
+
# A line that starts with the type ('[') or the value (':') has no
|
|
48
|
+
# name: the name is nil, and the whole line is left for the type
|
|
49
|
+
# and value.
|
|
50
|
+
#
|
|
46
51
|
def detect_name
|
|
47
52
|
@line = @line.lstrip.chomp
|
|
53
|
+
if @line.start_with?( '[', ':' )
|
|
54
|
+
@name = nil
|
|
55
|
+
@line = " #{@line}"
|
|
56
|
+
@idx = 0
|
|
57
|
+
return
|
|
58
|
+
end
|
|
59
|
+
|
|
48
60
|
@idx = @line.index( ' ' )
|
|
49
61
|
@idx = 0 unless @idx
|
|
50
62
|
@name = @line[ 0..@idx - 1 ]
|
|
@@ -58,6 +70,12 @@ module Gloo
|
|
|
58
70
|
# reproduce a declaration's original spacing when its value
|
|
59
71
|
# hasn't changed.
|
|
60
72
|
#
|
|
73
|
+
# A type that opens with '[' but has no ']' (eg. 'a [int : 3') is
|
|
74
|
+
# read up to the next space, as if it were closed there; the tail
|
|
75
|
+
# is everything after it, and missing_bracket is set so the loader
|
|
76
|
+
# can report it. A ']' later on the line (in the value) doesn't
|
|
77
|
+
# count.
|
|
78
|
+
#
|
|
61
79
|
def detect_type
|
|
62
80
|
@line = @line[ @idx + 1..-1 ]
|
|
63
81
|
@idx = @line.index( ' ' )
|
|
@@ -68,13 +86,32 @@ module Gloo
|
|
|
68
86
|
return
|
|
69
87
|
end
|
|
70
88
|
|
|
71
|
-
|
|
72
|
-
|
|
89
|
+
word = @line[ 0..( @idx ? @idx - 1 : -1 ) ]
|
|
90
|
+
return detect_bracketed_type( word ) if word[ 0 ] == '['
|
|
91
|
+
|
|
92
|
+
@type = word
|
|
73
93
|
@type = @type[ 0..-2 ] if @type[ -1 ] == ']'
|
|
74
94
|
close = @line.index( ']' )
|
|
75
95
|
@raw_tail = close ? @line[ close + 1..-1 ] : ''
|
|
76
96
|
end
|
|
77
97
|
|
|
98
|
+
#
|
|
99
|
+
# The type word starts with '['. The type ends at the ']' in that
|
|
100
|
+
# word (which may be followed directly by the ':', as in
|
|
101
|
+
# 'a [int]: 3'); with no ']' in it, the whole word is the type.
|
|
102
|
+
#
|
|
103
|
+
def detect_bracketed_type( word )
|
|
104
|
+
close = word.index( ']' )
|
|
105
|
+
if close
|
|
106
|
+
@type = word[ 1...close ]
|
|
107
|
+
@raw_tail = @line[ close + 1..-1 ]
|
|
108
|
+
else
|
|
109
|
+
@missing_bracket = true
|
|
110
|
+
@type = word[ 1..-1 ]
|
|
111
|
+
@raw_tail = @idx ? @line[ @idx..-1 ] : ''
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
78
115
|
#
|
|
79
116
|
# Detect the object value.
|
|
80
117
|
# Use nil if there is no value specified.
|