gloo 7.0.4 → 7.1.1

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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -1
  3. data/docs/application.md +16 -2
  4. data/docs/development.md +1 -1
  5. data/docs/iterators.md +24 -0
  6. data/docs/language_objects.md +34 -0
  7. data/docs/language_syntax.md +26 -1
  8. data/docs/objects.md +1 -1
  9. data/docs/plugins.md +10 -4
  10. data/docs/testing.md +206 -0
  11. data/docs/verbs.md +7 -0
  12. data/lib/VERSION +1 -1
  13. data/lib/VERSION_NOTES +18 -2
  14. data/lib/gloo/app/engine.rb +19 -9
  15. data/lib/gloo/app/log.rb +2 -2
  16. data/lib/gloo/app/settings.rb +20 -3
  17. data/lib/gloo/core/gloo_system.rb +7 -0
  18. data/lib/gloo/core/invoker.rb +20 -7
  19. data/lib/gloo/core/obj.rb +22 -1
  20. data/lib/gloo/core/tokens.rb +20 -0
  21. data/lib/gloo/core/verb.rb +31 -0
  22. data/lib/gloo/exec/exec_env.rb +6 -3
  23. data/lib/gloo/exec/script.rb +7 -0
  24. data/lib/gloo/objs/basic/script.rb +9 -0
  25. data/lib/gloo/objs/basic/string_msgs.rb +6 -7
  26. data/lib/gloo/objs/basic/untyped.rb +2 -1
  27. data/lib/gloo/objs/ctrl/each.rb +4 -2
  28. data/lib/gloo/objs/ctrl/each_child.rb +1 -1
  29. data/lib/gloo/objs/ctrl/each_dir.rb +1 -1
  30. data/lib/gloo/objs/ctrl/each_file.rb +1 -1
  31. data/lib/gloo/objs/ctrl/each_line.rb +1 -1
  32. data/lib/gloo/objs/ctrl/each_word.rb +1 -1
  33. data/lib/gloo/objs/ctrl/repeat.rb +5 -3
  34. data/lib/gloo/objs/str_utils/cipher.rb +10 -2
  35. data/lib/gloo/objs/system/file_handle.rb +5 -2
  36. data/lib/gloo/objs/web/json.rb +7 -5
  37. data/lib/gloo/persist/declaration_ledger.rb +6 -6
  38. data/lib/gloo/persist/file_loader.rb +11 -1
  39. data/lib/gloo/persist/file_storage.rb +8 -2
  40. data/lib/gloo/persist/persist_man.rb +29 -1
  41. data/lib/gloo/verbs/break.rb +7 -2
  42. data/lib/gloo/verbs/context.rb +10 -2
  43. data/lib/gloo/verbs/eval.rb +5 -4
  44. data/lib/gloo/verbs/exists.rb +14 -2
  45. data/lib/gloo/verbs/invoke.rb +7 -6
  46. data/lib/gloo/verbs/load.rb +16 -2
  47. data/lib/gloo/verbs/log.rb +8 -4
  48. data/lib/gloo/verbs/save.rb +10 -3
  49. data/lib/gloo/verbs/show.rb +15 -10
  50. data/test.gloo/ctrl/each.test.gloo +21 -7
  51. data/test.gloo/lang/declaration.test.gloo +7 -2
  52. data/test.gloo/lang/exceptions.test.gloo +22 -0
  53. data/test.gloo/lang/gloo_sys.test.gloo +12 -0
  54. data/test.gloo/lang/naming.test.gloo +7 -2
  55. data/test.gloo/objs/cipher.test.gloo +27 -0
  56. data/test.gloo/objs/file.test.gloo +18 -2
  57. data/test.gloo/objs/fixtures/source_dir_fixture.gloo +6 -0
  58. data/test.gloo/objs/int.test.gloo +6 -3
  59. data/test.gloo/objs/json.test.gloo +2 -0
  60. data/test.gloo/objs/obj.test.gloo +33 -0
  61. data/test.gloo/objs/string.test.gloo +2 -2
  62. data/test.gloo/verbs/break.test.gloo +66 -0
  63. data/test.gloo/verbs/context.test.gloo +12 -0
  64. data/test.gloo/verbs/eval.test.gloo +8 -0
  65. data/test.gloo/verbs/exists.test.gloo +19 -0
  66. data/test.gloo/verbs/invoke.test.gloo +50 -0
  67. data/test.gloo/verbs/load.test.gloo +101 -2
  68. data/test.gloo/verbs/log.test.gloo +16 -0
  69. data/test.gloo/verbs/reload.test.gloo +9 -4
  70. data/test.gloo/verbs/save.test.gloo +41 -16
  71. data/test.gloo/verbs/show.test.gloo +32 -0
  72. metadata +5 -6
@@ -5,6 +5,7 @@
5
5
  #
6
6
 
7
7
  require 'yaml'
8
+ require 'fileutils'
8
9
 
9
10
  module Gloo
10
11
  module App
@@ -137,9 +138,6 @@ module Gloo
137
138
  @ext_path = File.join( @user_root, 'extensions' )
138
139
  Dir.mkdir( @ext_path ) unless File.exist?( @ext_path )
139
140
 
140
- @tmp_path = File.join( @user_root, 'tmp' )
141
- Dir.mkdir( @tmp_path ) unless File.exist?( @tmp_path )
142
-
143
141
  @debug_path = File.join( @user_root, 'debug' )
144
142
  Dir.mkdir( @debug_path ) unless File.exist?( @debug_path )
145
143
  end
@@ -156,6 +154,8 @@ module Gloo
156
154
  @project_path = File.join( @user_root, 'projects' )
157
155
  end
158
156
 
157
+ init_tmp_path( settings[ 'gloo' ][ 'tmp_path' ] )
158
+
159
159
  @start_with = settings[ 'gloo' ][ 'start_with' ]
160
160
  @list_indent = settings[ 'gloo' ][ 'list_indent' ]
161
161
  @list_levels = settings[ 'gloo' ][ 'list_levels' ]
@@ -167,6 +167,16 @@ module Gloo
167
167
  @theme = DEFAULT_THEME unless THEMES.include?( @theme )
168
168
  end
169
169
 
170
+ #
171
+ # Set the tmp folder: the tmp_path setting if there is one (a
172
+ # leading ~ is expanded), or tmp under the user root. Created
173
+ # if it isn't there yet.
174
+ #
175
+ def init_tmp_path( path )
176
+ @tmp_path = path.blank? ? File.join( @user_root, 'tmp' ) : File.expand_path( path.to_s )
177
+ FileUtils.mkdir_p( @tmp_path ) unless File.exist?( @tmp_path )
178
+ end
179
+
170
180
  #
171
181
  # Get the app's required directories.
172
182
  #
@@ -188,6 +198,7 @@ module Gloo
188
198
  #
189
199
  def get_default_settings
190
200
  projects = File.join( @user_root, 'projects' )
201
+ tmp = File.join( @user_root, 'tmp' )
191
202
  str = <<~TEXT
192
203
  #
193
204
  # Gloo configuration
@@ -200,6 +211,12 @@ module Gloo
200
211
  #
201
212
  project_path: #{projects}
202
213
 
214
+ #
215
+ # Folder for temporary files, such as scratch files written by
216
+ # tests. Scripts get it with $.gloo_tmp.
217
+ #
218
+ tmp_path: #{tmp}
219
+
203
220
  #
204
221
  # Run this script when starting up gloo.
205
222
  # (Only if a script file is not specified.)
@@ -179,6 +179,11 @@ module Gloo
179
179
  return @engine.settings.project_path
180
180
  end
181
181
 
182
+ # Get the Gloo tmp directory
183
+ def msg_gloo_tmp
184
+ return @engine.settings.tmp_path
185
+ end
186
+
182
187
  # The running app directory.
183
188
  def msg_app
184
189
  return @engine.settings.project_path
@@ -314,6 +319,8 @@ module Gloo
314
319
  'gloo_config — Get the gloo configuration directory.',
315
320
  'gloo_projects — Get the gloo projects directory.',
316
321
  'gloo_log — Get the gloo logging directory.',
322
+ 'gloo_tmp — Get the gloo tmp directory, for temporary files ' \
323
+ '(the tmp_path setting in gloo.yml).',
317
324
  'screen_lines — Get the number of lines on screen.',
318
325
  'screen_cols — Get the number of columns on screen.',
319
326
  'platform_cpu — Get the platform CPU.',
@@ -18,26 +18,39 @@ module Gloo
18
18
  #
19
19
  # Resolve, validate and invoke the function at the given
20
20
  # target path, with the given raw (unevaluated) arg tokens.
21
- # Reports an error and returns nil for any failure - including
22
- # a failure inside the function itself (see Function#invoke,
23
- # which leaves `it` alone rather than setting it to an
24
- # unreliable result when that happens).
21
+ # For any failure, reports an error, puts false in `it` and
22
+ # returns nil - before the call (see failed), or inside the
23
+ # function itself (see Function#invoke).
25
24
  #
26
25
  def self.invoke( engine, target, arg_tokens )
27
26
  if target.nil? || target.to_s.strip.empty?
28
27
  engine.syntax_err NO_TARGET_ERR
29
- return nil
28
+ return failed( engine )
30
29
  end
31
30
 
32
31
  func = resolve_function( engine, target )
33
- return nil unless func
32
+ return failed( engine ) unless func
34
33
 
34
+ # An arg that couldn't be worked out was already reported, but
35
+ # the function's own error tracking would clear it if the call
36
+ # went ahead.
37
+ errors_before = engine.heap.error.error_count
35
38
  args = evaluate_arg_tokens( engine, arg_tokens )
36
- return nil unless params_count_ok?( engine, func, args )
39
+ return failed( engine ) if engine.heap.error.error_count > errors_before
40
+ return failed( engine ) unless params_count_ok?( engine, func, args )
37
41
 
38
42
  return invoke_function( engine, func, args )
39
43
  end
40
44
 
45
+ #
46
+ # The function couldn't be called (the problem was already
47
+ # reported): put false in `it`, and return nil.
48
+ #
49
+ def self.failed( engine )
50
+ engine.heap.it.set_to false
51
+ return nil
52
+ end
53
+
41
54
  #
42
55
  # Resolve the function object at the target path. Reports an
43
56
  # error and returns nil if the path doesn't resolve to
data/lib/gloo/core/obj.rb CHANGED
@@ -338,7 +338,7 @@ module Gloo
338
338
  # Get a list of message names that this object receives.
339
339
  #
340
340
  def self.messages
341
- return %w[save reload unload blank? contains? responds_to? doc]
341
+ return %w[save reload unload blank? contains? responds_to? doc source_file source_dir]
342
342
  end
343
343
 
344
344
  #
@@ -492,6 +492,27 @@ module Gloo
492
492
  return self.doc
493
493
  end
494
494
 
495
+ #
496
+ # The path of the file this object was loaded from, into 'it';
497
+ # blank if it didn't come from a file.
498
+ #
499
+ def msg_source_file
500
+ path = @engine.persist_man.source_file_of( self ).to_s
501
+ @engine.heap.it.set_to path
502
+ return path
503
+ end
504
+
505
+ #
506
+ # The folder of the file this object was loaded from, with no
507
+ # trailing slash, into 'it'; blank if it didn't come from a file.
508
+ #
509
+ def msg_source_dir
510
+ path = @engine.persist_man.source_file_of( self )
511
+ dir = path ? File.dirname( path ) : ''
512
+ @engine.heap.it.set_to dir
513
+ return dir
514
+ end
515
+
495
516
 
496
517
  # ---------------------------------------------------------------------
497
518
  # Render
@@ -176,6 +176,17 @@ module Gloo
176
176
  return nil
177
177
  end
178
178
 
179
+ #
180
+ # A copy of these tokens with one more token added at the end,
181
+ # taken as is (not split on spaces or quotes). The original is
182
+ # left alone.
183
+ #
184
+ def with_token( token )
185
+ copy = dup
186
+ copy.append_token token
187
+ return copy
188
+ end
189
+
179
190
  #
180
191
  # Get the item after a given token.
181
192
  #
@@ -186,6 +197,15 @@ module Gloo
186
197
  return nil
187
198
  end
188
199
 
200
+ protected
201
+
202
+ #
203
+ # Add a token at the end, in a new list (see with_token).
204
+ #
205
+ def append_token( token )
206
+ @tokens += [ token ]
207
+ end
208
+
189
209
  # ---------------------------------------------------------------------
190
210
  # Private functions
191
211
  # ---------------------------------------------------------------------
@@ -83,6 +83,37 @@ module Gloo
83
83
  return true
84
84
  end
85
85
 
86
+ #
87
+ # For a verb whose arguments are literal text (a file name, a
88
+ # path, a name): a trailing ( expr ) is worked out and its value
89
+ # added as the last argument, so the text can be computed, as in
90
+ # load ( $.gloo_tmp '/x.gloo' ). The parser has already split the
91
+ # parenthetical off into params. Returns false if the expression
92
+ # couldn't be worked out (already reported), true otherwise.
93
+ #
94
+ def take_evaluated_arg
95
+ return true unless @params
96
+
97
+ value, ok = evaluate_expr( @params.tokens )
98
+ return false unless ok
99
+
100
+ @tokens = @tokens.with_token( value.to_s )
101
+ @params = nil
102
+ return true
103
+ end
104
+
105
+ #
106
+ # Work out an expression from the given tokens. Returns the value
107
+ # and whether it worked: false if the expression reported a
108
+ # problem (already reported), in which case the value is whatever
109
+ # it had worked out so far.
110
+ #
111
+ def evaluate_expr( tokens )
112
+ errors_before = @engine.heap.error.error_count
113
+ value = Gloo::Expr::Expression.new( @engine, tokens ).evaluate
114
+ return value, @engine.heap.error.error_count == errors_before
115
+ end
116
+
86
117
  #
87
118
  # Run the verb.
88
119
  #
@@ -35,13 +35,16 @@ module Gloo
35
35
  end
36
36
 
37
37
  #
38
- # Where the line now running is, for error messages: the running
39
- # script and its line, or the file and line being loaded.
38
+ # Where the line now running is, for error messages: the file and
39
+ # line being loaded, or the running script and its line. While a
40
+ # file is being read, a problem is in that file, even when a
41
+ # script started the load.
40
42
  #
41
43
  def current_location
44
+ return @load_location if @load_location
42
45
  return @running_script.location if @running_script
43
46
 
44
- return @load_location
47
+ return nil
45
48
  end
46
49
 
47
50
  #
@@ -65,6 +65,13 @@ module Gloo
65
65
  @break_out = true
66
66
  end
67
67
 
68
+ #
69
+ # Did a break end this script's run?
70
+ #
71
+ def broke_out?
72
+ return @break_out
73
+ end
74
+
68
75
  end
69
76
  end
70
77
  end
@@ -115,6 +115,15 @@ module Gloo
115
115
  def msg_run
116
116
  s = Gloo::Exec::Script.new( @engine, self )
117
117
  s.run
118
+ @broke_out = s.broke_out?
119
+ end
120
+
121
+ #
122
+ # Did a break end this script's last run? A loop uses this to
123
+ # stop when its do script breaks.
124
+ #
125
+ def broke_out?
126
+ return @broke_out == true
118
127
  end
119
128
 
120
129
  # ---------------------------------------------------------------------
@@ -57,7 +57,7 @@ module Gloo
57
57
  'split ({from} {to}) — Get the substring from index {from} up to (not including) index {to}. Indexes are 0-based; out-of-range indexes are clamped to the start or end of the string. Both parameters are required. Does not change the value of the string. It will have the substring.',
58
58
  'splitl ({index}) — Get the substring to the left of index {index} (same as split (0, {index})). A parameter is required. Does not change the value of the string. It will have the substring.',
59
59
  'splitr ({index}) — Get the substring from index {index} to the end of the string (same as split ({index}, size)). A parameter is required. Does not change the value of the string. It will have the substring.',
60
- 'split_list ({delim} {dst.path}) — Split the string by {delim} and put the parts into children of the container at {dst.path} (or an alias that points to one), one part per child, in order. Existing children are matched by position and have their values set; extra parts get new (untyped) children, numbered from 1; extra existing children are left alone. Both parameters are required. Does not change the value of the string. It will have the number of parts.',
60
+ 'split_list ({delim} {dst.path}) — Split the string by {delim} and put the parts into children of the container at {dst.path} (or an alias that points to one), one part per child, in order. Existing children are matched by position and have their values set; extra parts get new (untyped) children, numbered from 1; extra existing children are left alone. Both parameters are required. Does not change the value of the string. It will be true (also when the string is empty), or false if a parameter or the target is bad.',
61
61
  'word_wrap ({width}) — Word-wrap the string to {width} columns, breaking on whitespace (never mid-word; an overlong word is left on its own line rather than broken). The {width} parameter is optional; it defaults to the terminal\'s current width. This message changes the value of the string. It will have the wrapped string.',
62
62
  'page — Show the value in a pager (less), for viewing long content a screen at a time.'
63
63
  ]
@@ -239,11 +239,12 @@ module Gloo
239
239
  # have their values set; if there are more parts than children,
240
240
  # new (untyped) children are created for the extras, numbered
241
241
  # from 1. Existing children beyond the part count are left alone.
242
- # The string's own value is unchanged; the number of parts is put
243
- # into 'it'.
242
+ # The string's own value is unchanged. 'it' is true once the parts
243
+ # are in place (also when there's nothing to split), or false if
244
+ # the parameters or the target are bad.
244
245
  #
245
246
  def msg_split_list
246
- return @engine.heap.it.set_to( 0 ) unless value
247
+ return @engine.heap.it.set_to( true ) unless value
247
248
 
248
249
  if @params&.token_count.to_i < 2
249
250
  @engine.syntax_err MISSING_PARAM_MSG
@@ -265,9 +266,7 @@ module Gloo
265
266
  child.set_value part
266
267
  end
267
268
 
268
- count = parts.count
269
- @engine.heap.it.set_to count
270
- return count
269
+ return @engine.heap.it.set_to( true )
271
270
  end
272
271
 
273
272
  #
@@ -50,7 +50,8 @@ module Gloo
50
50
  :description => 'An object with no type. A type declares ' \
51
51
  'which messages an object can receive; an untyped object ' \
52
52
  'receives only the base object messages (blank?, ' \
53
- 'contains?, responds_to?, reload, unload) but can hold a ' \
53
+ 'contains?, responds_to?, doc, source_dir, source_file, ' \
54
+ 'reload, unload) but can hold a ' \
54
55
  'value of any kind. If no type is specified when an object ' \
55
56
  'is created, it is untyped. Prefer untyped when you only ' \
56
57
  'need to hold, compare, or show a value and do not need ' \
@@ -43,13 +43,15 @@ module Gloo
43
43
  end
44
44
 
45
45
  #
46
- # Run the do script once.
46
+ # Run the do script once. Returns true if a break in it ended the
47
+ # run, so the loop should stop.
47
48
  #
48
49
  def run_do
49
50
  o = find_child DO
50
- return unless o
51
+ return false unless o
51
52
 
52
53
  Gloo::Exec::Dispatch.message( @engine, 'run', o )
54
+ return o.respond_to?( :broke_out? ) && o.broke_out?
53
55
  end
54
56
 
55
57
 
@@ -130,7 +130,7 @@ module Gloo
130
130
  end
131
131
 
132
132
  set_child child
133
- @iterator_obj.run_do
133
+ break if @iterator_obj.run_do
134
134
  end
135
135
 
136
136
  if group_mode && !first_time
@@ -58,7 +58,7 @@ module Gloo
58
58
  next unless File.directory?( f )
59
59
 
60
60
  set_dir f
61
- @iterator_obj.run_do
61
+ break if @iterator_obj.run_do
62
62
  end
63
63
  end
64
64
 
@@ -60,7 +60,7 @@ module Gloo
60
60
  next unless include?( f )
61
61
 
62
62
  set_file f
63
- @iterator_obj.run_do
63
+ break if @iterator_obj.run_do
64
64
  end
65
65
  end
66
66
 
@@ -48,7 +48,7 @@ module Gloo
48
48
 
49
49
  str.each_line do |line|
50
50
  set_line line
51
- @iterator_obj.run_do
51
+ break if @iterator_obj.run_do
52
52
  end
53
53
  end
54
54
 
@@ -48,7 +48,7 @@ module Gloo
48
48
 
49
49
  str.split( ' ' ).each do |word|
50
50
  set_word word
51
- @iterator_obj.run_do
51
+ break if @iterator_obj.run_do
52
52
  end
53
53
  end
54
54
 
@@ -37,13 +37,15 @@ module Gloo
37
37
  end
38
38
 
39
39
  #
40
- # Run the do script once.
40
+ # Run the do script once. Returns true if a break in it ended the
41
+ # run, so the loop should stop.
41
42
  #
42
43
  def run_do
43
44
  o = find_child DO
44
- return unless o
45
+ return false unless o
45
46
 
46
47
  Gloo::Exec::Dispatch.message( @engine, 'run', o )
48
+ return o.respond_to?( :broke_out? ) && o.broke_out?
47
49
  end
48
50
 
49
51
  #
@@ -98,7 +100,7 @@ module Gloo
98
100
  def msg_run
99
101
  times.times do |index|
100
102
  set_index index
101
- run_do
103
+ break if run_do
102
104
  end
103
105
  end
104
106
 
@@ -209,7 +209,8 @@ module Gloo
209
209
  {
210
210
  :name => KEYWORD,
211
211
  :shortcut => KEYWORD_SHORT,
212
- :description => 'Tool to encrypt and decrypt data.',
212
+ :description => 'Tool to encrypt and decrypt data, with ' \
213
+ 'AES-256-CBC.',
213
214
  :children => [
214
215
  'key (string) — The encryption key. Base64 encoded.',
215
216
  'init_vector (string) — The initialization vector. Base64 encoded.',
@@ -218,8 +219,15 @@ module Gloo
218
219
  :messages => [
219
220
  'generate_keys — Generate an encryption key and initialization vector. The keys are base64 encoded.',
220
221
  'encrypt — Encrypt the data. The data child is changed. It will have the encrypted data.',
221
- 'decrypt — Decrypt the data. The data child is changed. It will have the decrypted data.'
222
+ 'decrypt — Decrypt the data. The data child is changed. It will have the decrypted data. With the wrong key or init_vector, decrypt usually reports an error and it is false; but not always (see notes).'
222
223
  ],
224
+ :notes => 'AES-CBC can\'t always tell that a key is wrong. A ' \
225
+ 'wrong key usually leaves the decrypted data with padding ' \
226
+ 'that doesn\'t check out, which is reported as an error. But ' \
227
+ 'about 1 time in 256 the padding happens to look right: then ' \
228
+ 'decrypt reports nothing, and the data is garbage rather than ' \
229
+ 'the original. To be sure the key is right, keep a value you ' \
230
+ 'can check, such as encrypting a known word alongside the data.',
223
231
  :examples => <<~EXAMPLES.strip
224
232
  #
225
233
  # Encrypt and Decrypt a string.
@@ -74,7 +74,9 @@ module Gloo
74
74
  end
75
75
 
76
76
  #
77
- # Read the contents of the file into the object.
77
+ # Read the contents of the file. With a destination, the contents
78
+ # go into it and 'it' is true; with none, the contents go in 'it'.
79
+ # 'it' is false if the file can't be read.
78
80
  #
79
81
  def msg_read
80
82
  return @engine.heap.it.set_to( false ) unless check_file_exists?
@@ -91,6 +93,7 @@ module Gloo
91
93
  end
92
94
 
93
95
  o.set_value data
96
+ return @engine.heap.it.set_to( true )
94
97
  end
95
98
  @engine.heap.it.set_to data
96
99
  end
@@ -290,7 +293,7 @@ module Gloo
290
293
  'on disk. The string value of the file object is the path ' \
291
294
  'and name of the file.',
292
295
  :messages => [
293
- 'read ({into.obj}) — Read the file and put the data in the specified object. If {into.obj} is not specified, the data will be in it. If {into.obj} does not exist, that is an error.',
296
+ 'read ({into.obj}) — Read the file and put the data in the specified object; it will be true. If {into.obj} is not specified, the data will be in it. If {into.obj} does not exist, or the file can\'t be read, that is an error and it will be false.',
294
297
  'write ({from.obj}) — Write the data in {from.obj} into the file.',
295
298
  'append ({data}) — Append the given data to the file as a new line. Adds a leading newline first if the file doesn\'t already end with one.',
296
299
  'delete — Delete the file.',
@@ -131,26 +131,28 @@ module Gloo
131
131
  #
132
132
  # Parse the JSON data and put it in objects.
133
133
  # The additional parameter is the path to the destination
134
- # for the parsed objects.
134
+ # for the parsed objects. 'it' is true once they're built, or
135
+ # false if the destination or the JSON is bad.
135
136
  #
136
137
  def msg_parse
137
138
  if @params&.token_count&.positive?
138
139
  pn = Gloo::Core::Pn.new( @engine, @params.tokens.first )
139
140
  unless pn&.exists?
140
141
  @engine.err Gloo::Core::NotFound.object( @params.tokens.first )
141
- return
142
+ return @engine.heap.it.set_to( false )
142
143
  end
143
144
  else
144
145
  @engine.syntax_err 'Destination path for parsed objects is required'
145
- return
146
+ return @engine.heap.it.set_to( false )
146
147
  end
147
148
  parent = pn.resolve
148
149
 
149
150
  json = nil
150
- return unless attempt( "parse the JSON in #{self.pn}", JSON::ParserError, set_it: false ) do
151
+ return false unless attempt( "parse the JSON in #{self.pn}", JSON::ParserError ) do
151
152
  json = JSON.parse( self.value )
152
153
  end
153
154
  self.handle_json_to_obj( json, parent )
155
+ return @engine.heap.it.set_to( true )
154
156
  end
155
157
 
156
158
  # ---------------------------------------------------------------------
@@ -240,7 +242,7 @@ module Gloo
240
242
  :messages => [
241
243
  '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.',
242
244
  '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.',
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).',
245
+ 'parse ({dst.path}) — Parse the JSON data and put values in the object specified by the parameter; it will be true. If the destination is missing or the JSON is bad, that is an error and it will be false. Example: tell myjson to parse (path.to.dst).',
244
246
  'pretty — Make the JSON format pretty. The value of the json is changed. It will have the pretty JSON.'
245
247
  ],
246
248
  :examples => <<~EXAMPLES.strip
@@ -14,11 +14,10 @@ module Gloo
14
14
  attr_reader :roots
15
15
 
16
16
  #
17
- # Set up a ledger for the file at pn.
17
+ # Set up a ledger for one file's load.
18
18
  #
19
- def initialize( engine, pn )
19
+ def initialize( engine )
20
20
  @engine = engine
21
- @pn = pn
22
21
  @roots = []
23
22
  @mine = {}.compare_by_identity
24
23
  end
@@ -43,15 +42,16 @@ module Gloo
43
42
  # `value`. If another file created it and the values differ, the
44
43
  # new value is being ignored -- warn so it isn't a surprise. A
45
44
  # duplicate name inside one file is left alone (a documented
46
- # "first one wins").
45
+ # "first one wins"). The warning's location (this file and line)
46
+ # comes from the load.
47
47
  #
48
48
  def clash( prior, name, value )
49
49
  return if @mine.key?( prior )
50
50
  return if value.nil? || value.to_s.strip.empty?
51
51
  return if prior.value.to_s == value.to_s
52
52
 
53
- @engine.log.warn(
54
- "#{@pn}: '#{name}' is already declared in another loaded file with a " \
53
+ @engine.warn(
54
+ "'#{name}' is already declared in another loaded file with a " \
55
55
  "different value (#{prior.value.inspect} kept, #{value.inspect} ignored)."
56
56
  )
57
57
  end
@@ -48,11 +48,12 @@ module Gloo
48
48
  @mech = @engine.platform.get_file_mech( @engine )
49
49
  @pn = pn
50
50
  @obj = nil
51
+ @found = false
51
52
  @source_doc = Gloo::Persist::Source::SourceDoc.new
52
53
  @trivia = Gloo::Persist::TriviaBuffer.new
53
54
  @body = Gloo::Persist::ScriptBodyCollector.new
54
55
  @shorthand = Gloo::Persist::ShorthandExpander.new( engine )
55
- @ledger = Gloo::Persist::DeclarationLedger.new( engine, pn )
56
+ @ledger = Gloo::Persist::DeclarationLedger.new( engine )
56
57
  @in_block = false
57
58
  @block_value = ''
58
59
  @body_started = false
@@ -66,6 +67,14 @@ module Gloo
66
67
  return @ledger.roots
67
68
  end
68
69
 
70
+ #
71
+ # Was the file there to read? False before load, or when the
72
+ # file couldn't be found (already reported as an error).
73
+ #
74
+ def found?
75
+ return @found
76
+ end
77
+
69
78
  #
70
79
  # Load the objects from the file.
71
80
  #
@@ -74,6 +83,7 @@ module Gloo
74
83
  @engine.err Gloo::Core::NotFound.file( @pn )
75
84
  return
76
85
  end
86
+ @found = true
77
87
 
78
88
  @engine.log.debug "Loading file '#{@pn}'"
79
89
  @indent_stack = IndentStack.new( @engine.heap.root, @source_doc )
@@ -8,6 +8,8 @@ module Gloo
8
8
  module Persist
9
9
  class FileStorage
10
10
 
11
+ NO_OBJECTS_WARNING = 'No objects in file; nothing loaded from '.freeze
12
+
11
13
  attr_reader :obj, :pn, :roots, :source_doc
12
14
 
13
15
  #
@@ -50,6 +52,10 @@ module Gloo
50
52
 
51
53
  #
52
54
  # Load the object from the file.
55
+ # A file that has no objects (empty, comments only, or only
56
+ # 'load lib' lines) isn't an error, so it's reported as a
57
+ # warning. A file that couldn't be found was already reported
58
+ # by the loader.
53
59
  #
54
60
  def load
55
61
  fl = FileLoader.new( @engine, @pn )
@@ -59,8 +65,8 @@ module Gloo
59
65
  @source_doc = fl.source_doc
60
66
  if @obj
61
67
  @engine.log.debug "Loaded object: #{@obj.name}"
62
- else
63
- @engine.err "Error loading file at #{@pn}"
68
+ elsif fl.found?
69
+ @engine.warn "#{NO_OBJECTS_WARNING}#{@pn}"
64
70
  end
65
71
  end
66
72