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.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +1 -0
  3. data/docs/application.md +3 -1
  4. data/docs/iterators.md +39 -3
  5. data/docs/language_syntax.md +53 -5
  6. data/docs/objects.md +58 -1
  7. data/docs/operators.md +16 -0
  8. data/docs/plugins.md +9 -1
  9. data/docs/verbs.md +4 -2
  10. data/lib/VERSION +1 -1
  11. data/lib/VERSION_NOTES +20 -0
  12. data/lib/gloo/app/engine.rb +64 -8
  13. data/lib/gloo/app/log.rb +47 -6
  14. data/lib/gloo/convert/converter.rb +63 -2
  15. data/lib/gloo/convert/string_to_date.rb +14 -0
  16. data/lib/gloo/convert/string_to_datetime.rb +16 -0
  17. data/lib/gloo/convert/string_to_decimal.rb +13 -0
  18. data/lib/gloo/convert/string_to_integer.rb +9 -2
  19. data/lib/gloo/convert/string_to_time.rb +28 -0
  20. data/lib/gloo/core/error.rb +11 -2
  21. data/lib/gloo/core/event_manager.rb +7 -4
  22. data/lib/gloo/core/factory.rb +16 -11
  23. data/lib/gloo/core/here.rb +6 -1
  24. data/lib/gloo/core/invoker.rb +2 -3
  25. data/lib/gloo/core/not_found.rb +42 -0
  26. data/lib/gloo/core/obj.rb +28 -2
  27. data/lib/gloo/core/op.rb +2 -0
  28. data/lib/gloo/core/parser.rb +1 -1
  29. data/lib/gloo/core/pn.rb +33 -12
  30. data/lib/gloo/core/tokens.rb +38 -0
  31. data/lib/gloo/core/verb.rb +54 -0
  32. data/lib/gloo/exec/dispatch.rb +5 -6
  33. data/lib/gloo/exec/exec_env.rb +13 -0
  34. data/lib/gloo/exec/runner.rb +10 -2
  35. data/lib/gloo/exec/script.rb +13 -1
  36. data/lib/gloo/expr/expression.rb +39 -1
  37. data/lib/gloo/expr/op_plus.rb +3 -0
  38. data/lib/gloo/objs/basic/boolean.rb +16 -0
  39. data/lib/gloo/objs/basic/container.rb +154 -6
  40. data/lib/gloo/objs/basic/decimal.rb +1 -0
  41. data/lib/gloo/objs/basic/integer.rb +1 -0
  42. data/lib/gloo/objs/basic/string.rb +0 -1
  43. data/lib/gloo/objs/basic/string_msgs.rb +68 -32
  44. data/lib/gloo/objs/ctrl/each.rb +22 -3
  45. data/lib/gloo/objs/ctrl/each_dir.rb +20 -7
  46. data/lib/gloo/objs/ctrl/each_file.rb +78 -9
  47. data/lib/gloo/objs/ctrl/function.rb +6 -5
  48. data/lib/gloo/objs/dt/date.rb +28 -11
  49. data/lib/gloo/objs/dt/datetime.rb +72 -21
  50. data/lib/gloo/objs/dt/dt_tools.rb +46 -2
  51. data/lib/gloo/objs/dt/time.rb +28 -11
  52. data/lib/gloo/objs/str_utils/cipher.rb +17 -4
  53. data/lib/gloo/objs/str_utils/password.rb +6 -2
  54. data/lib/gloo/objs/system/erb.rb +9 -3
  55. data/lib/gloo/objs/system/file_handle.rb +57 -21
  56. data/lib/gloo/objs/system/system.rb +6 -2
  57. data/lib/gloo/objs/web/http_get.rb +12 -2
  58. data/lib/gloo/objs/web/http_post.rb +6 -2
  59. data/lib/gloo/objs/web/json.rb +38 -18
  60. data/lib/gloo/objs/web/uri.rb +36 -16
  61. data/lib/gloo/persist/file_loader.rb +72 -11
  62. data/lib/gloo/persist/file_saver.rb +2 -1
  63. data/lib/gloo/persist/indent_stack.rb +49 -33
  64. data/lib/gloo/persist/line_splitter.rb +40 -3
  65. data/lib/gloo/persist/persist_man.rb +2 -4
  66. data/lib/gloo/plugin/ext_manager.rb +6 -9
  67. data/lib/gloo/plugin/lib_manager.rb +2 -2
  68. data/lib/gloo/verbs/break.rb +1 -0
  69. data/lib/gloo/verbs/check.rb +1 -1
  70. data/lib/gloo/verbs/cls.rb +1 -0
  71. data/lib/gloo/verbs/create.rb +32 -4
  72. data/lib/gloo/verbs/execute.rb +1 -1
  73. data/lib/gloo/verbs/exists.rb +27 -15
  74. data/lib/gloo/verbs/files.rb +2 -1
  75. data/lib/gloo/verbs/help.rb +1 -0
  76. data/lib/gloo/verbs/if.rb +1 -1
  77. data/lib/gloo/verbs/invoke.rb +1 -1
  78. data/lib/gloo/verbs/list.rb +4 -3
  79. data/lib/gloo/verbs/load.rb +3 -3
  80. data/lib/gloo/verbs/move.rb +10 -8
  81. data/lib/gloo/verbs/put.rb +20 -8
  82. data/lib/gloo/verbs/quit.rb +1 -0
  83. data/lib/gloo/verbs/redirect.rb +8 -2
  84. data/lib/gloo/verbs/reload.rb +9 -1
  85. data/lib/gloo/verbs/run.rb +2 -2
  86. data/lib/gloo/verbs/save.rb +4 -2
  87. data/lib/gloo/verbs/tell.rb +1 -1
  88. data/lib/gloo/verbs/unless.rb +1 -1
  89. data/lib/gloo/verbs/unload.rb +8 -0
  90. data/test.gloo/ctrl/each.test.gloo +88 -0
  91. data/test.gloo/lang/declaration.test.gloo +27 -0
  92. data/test.gloo/lang/exceptions.test.gloo +60 -0
  93. data/test.gloo/lang/failures.test.gloo +139 -0
  94. data/test.gloo/lang/it.test.gloo +133 -0
  95. data/test.gloo/lang/naming.test.gloo +11 -0
  96. data/test.gloo/lang/ops.test.gloo +30 -0
  97. data/test.gloo/objs/can.test.gloo +101 -0
  98. data/test.gloo/objs/string.test.gloo +24 -0
  99. data/test.gloo/string/str.test.gloo +28 -0
  100. data/test.gloo/verbs/create.test.gloo +36 -0
  101. data/test.gloo/verbs/exists.test.gloo +33 -1
  102. data/test.gloo/verbs/unload.test.gloo +34 -0
  103. 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 = `#{cmd}`
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 = Gloo::Objs::HttpGet.invoke_request( url, skip_ssl_verify? )
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 = Gloo::Objs::HttpPost.post_json( uri, body, skip_ssl_verify? )
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] :
@@ -65,12 +65,15 @@ module Gloo
65
65
  def msg_pretty
66
66
  return unless self.value
67
67
 
68
- json = JSON.parse( self.value )
69
- if self.value.start_with?( '"{' )
70
- json = JSON.parse( json )
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
- if @params&.token_count&.positive?
82
- expr = Gloo::Expr::Expression.new( @engine, @params.tokens )
83
- data = expr.evaluate
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 = Gloo::Objs::Json.get_value_in_json self.value, data
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 'Source path for objects does not exist'
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.err 'Source path for objects is required'
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 'Destination path for parsed objects does not exist'
140
+ @engine.err Gloo::Core::NotFound.object( @params.tokens.first )
128
141
  return
129
142
  end
130
143
  else
131
- @engine.err 'Destination path for parsed objects is required'
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 = JSON.parse( self.value )
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[ segment ]
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
  #
@@ -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
- return unless value
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
- uri = URI( value )
67
- response = Net::HTTP.start( uri.host, uri.port, :use_ssl => true )
68
- cert = response.peer_cert
69
- o = cert.not_after
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 unless value
85
+ return @engine.heap.it.set_to( '' ) if value.blank?
81
86
 
82
- o = URI( value ).fragment
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 unless value
100
+ return @engine.heap.it.set_to( '' ) if value.blank?
93
101
 
94
- o = URI( value ).query
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 unless value
115
+ return @engine.heap.it.set_to( '' ) if value.blank?
105
116
 
106
- o = URI( value ).path
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 unless value
130
+ return @engine.heap.it.set_to( '' ) if value.blank?
117
131
 
118
- o = URI( value ).host
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 unless value
145
+ return @engine.heap.it.set_to( '' ) if value.blank?
129
146
 
130
- o = URI( value ).scheme
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.log.warn 'Opening URL not supported on this platform.' if 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 "File '#{@pn}' does not exist."
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
- f = join_continuations( f )
76
- f.each_line { |line| dispatch_line( line ) }
77
- finish
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
- # Join continuation lines.
93
- #
94
- def join_continuations( data )
95
- return data.gsub( "\\\n", '' )
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 an unknown
238
- # type (factory.create logs a warning and returns nil).
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
- trial.set_value( node.raw_value )
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
- attr_reader :tabs
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
- @tabs = 0
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 = indent_delta( line_tabs )
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
- @parent_stack.pop
62
- @node_stack.pop
63
- end
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
- # Update @tabs for the new line and return the signed change in
71
- # nesting depth: positive if it's more indented than the last
72
- # line placed, negative if less, zero if the same.
73
- #
74
- def indent_delta( line_tabs )
75
- if line_tabs > @tabs
76
- # TODO: What if indent is more than one more level?
77
- @tabs = line_tabs
78
- return 1
79
- elsif line_tabs < @tabs
80
- diff = @tabs - line_tabs
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
- return 0
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
- @type = @line[ 0..( @idx ? @idx - 1 : -1 ) ]
72
- @type = @type[ 1..-1 ] if @type[ 0 ] == '['
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.