gloo 6.7.1 → 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 (105) 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 +24 -0
  12. data/lib/gloo/app/engine.rb +65 -9
  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/dictionary.rb +30 -21
  21. data/lib/gloo/core/error.rb +11 -2
  22. data/lib/gloo/core/event_manager.rb +7 -4
  23. data/lib/gloo/core/factory.rb +16 -11
  24. data/lib/gloo/core/here.rb +6 -1
  25. data/lib/gloo/core/invoker.rb +2 -3
  26. data/lib/gloo/core/not_found.rb +42 -0
  27. data/lib/gloo/core/obj.rb +28 -2
  28. data/lib/gloo/core/op.rb +2 -0
  29. data/lib/gloo/core/parser.rb +1 -1
  30. data/lib/gloo/core/pn.rb +33 -12
  31. data/lib/gloo/core/tokens.rb +38 -0
  32. data/lib/gloo/core/verb.rb +54 -0
  33. data/lib/gloo/exec/dispatch.rb +5 -6
  34. data/lib/gloo/exec/exec_env.rb +13 -0
  35. data/lib/gloo/exec/runner.rb +10 -2
  36. data/lib/gloo/exec/script.rb +13 -1
  37. data/lib/gloo/expr/expression.rb +39 -1
  38. data/lib/gloo/expr/op_plus.rb +3 -0
  39. data/lib/gloo/objs/basic/boolean.rb +16 -0
  40. data/lib/gloo/objs/basic/container.rb +154 -6
  41. data/lib/gloo/objs/basic/decimal.rb +1 -0
  42. data/lib/gloo/objs/basic/integer.rb +1 -0
  43. data/lib/gloo/objs/basic/string.rb +0 -1
  44. data/lib/gloo/objs/basic/string_msgs.rb +68 -32
  45. data/lib/gloo/objs/ctrl/each.rb +22 -3
  46. data/lib/gloo/objs/ctrl/each_dir.rb +20 -7
  47. data/lib/gloo/objs/ctrl/each_file.rb +78 -9
  48. data/lib/gloo/objs/ctrl/function.rb +6 -5
  49. data/lib/gloo/objs/dt/date.rb +28 -11
  50. data/lib/gloo/objs/dt/datetime.rb +72 -21
  51. data/lib/gloo/objs/dt/dt_tools.rb +46 -2
  52. data/lib/gloo/objs/dt/time.rb +28 -11
  53. data/lib/gloo/objs/str_utils/cipher.rb +17 -4
  54. data/lib/gloo/objs/str_utils/password.rb +6 -2
  55. data/lib/gloo/objs/system/erb.rb +9 -3
  56. data/lib/gloo/objs/system/file_handle.rb +57 -21
  57. data/lib/gloo/objs/system/system.rb +6 -2
  58. data/lib/gloo/objs/web/http_get.rb +12 -2
  59. data/lib/gloo/objs/web/http_post.rb +6 -2
  60. data/lib/gloo/objs/web/json.rb +38 -18
  61. data/lib/gloo/objs/web/uri.rb +36 -16
  62. data/lib/gloo/persist/file_loader.rb +72 -11
  63. data/lib/gloo/persist/file_saver.rb +2 -1
  64. data/lib/gloo/persist/indent_stack.rb +49 -33
  65. data/lib/gloo/persist/line_splitter.rb +40 -3
  66. data/lib/gloo/persist/persist_man.rb +2 -4
  67. data/lib/gloo/plugin/callback.rb +2 -2
  68. data/lib/gloo/plugin/ext_manager.rb +6 -9
  69. data/lib/gloo/plugin/lib_manager.rb +2 -2
  70. data/lib/gloo/verbs/break.rb +1 -0
  71. data/lib/gloo/verbs/check.rb +1 -1
  72. data/lib/gloo/verbs/cls.rb +1 -0
  73. data/lib/gloo/verbs/create.rb +32 -4
  74. data/lib/gloo/verbs/execute.rb +1 -1
  75. data/lib/gloo/verbs/exists.rb +27 -15
  76. data/lib/gloo/verbs/files.rb +2 -1
  77. data/lib/gloo/verbs/help.rb +1 -0
  78. data/lib/gloo/verbs/if.rb +1 -1
  79. data/lib/gloo/verbs/invoke.rb +1 -1
  80. data/lib/gloo/verbs/list.rb +4 -3
  81. data/lib/gloo/verbs/load.rb +3 -3
  82. data/lib/gloo/verbs/move.rb +10 -8
  83. data/lib/gloo/verbs/put.rb +20 -8
  84. data/lib/gloo/verbs/quit.rb +1 -0
  85. data/lib/gloo/verbs/redirect.rb +8 -2
  86. data/lib/gloo/verbs/reload.rb +9 -1
  87. data/lib/gloo/verbs/run.rb +2 -2
  88. data/lib/gloo/verbs/save.rb +4 -2
  89. data/lib/gloo/verbs/tell.rb +1 -1
  90. data/lib/gloo/verbs/unless.rb +1 -1
  91. data/lib/gloo/verbs/unload.rb +8 -0
  92. data/test.gloo/ctrl/each.test.gloo +88 -0
  93. data/test.gloo/lang/declaration.test.gloo +27 -0
  94. data/test.gloo/lang/exceptions.test.gloo +60 -0
  95. data/test.gloo/lang/failures.test.gloo +139 -0
  96. data/test.gloo/lang/it.test.gloo +133 -0
  97. data/test.gloo/lang/naming.test.gloo +11 -0
  98. data/test.gloo/lang/ops.test.gloo +30 -0
  99. data/test.gloo/objs/can.test.gloo +101 -0
  100. data/test.gloo/objs/string.test.gloo +24 -0
  101. data/test.gloo/string/str.test.gloo +28 -0
  102. data/test.gloo/verbs/create.test.gloo +36 -0
  103. data/test.gloo/verbs/exists.test.gloo +33 -1
  104. data/test.gloo/verbs/unload.test.gloo +34 -0
  105. metadata +6 -6
data/lib/gloo/core/pn.rb CHANGED
@@ -14,6 +14,8 @@ module Gloo
14
14
  ERROR = 'error'.freeze
15
15
  CONTEXT = '@'.freeze
16
16
  NAMED_COLORS = %w[red blue green white black yellow].freeze
17
+ IT_NOT_AN_OBJECT = "it isn't an object and can't be %s; %s.".freeze
18
+ IT_HINT = 'put it into an object first (put it into x)'.freeze
17
19
 
18
20
  attr_reader :src, :elements
19
21
 
@@ -61,6 +63,19 @@ module Gloo
61
63
  return @src.downcase == IT
62
64
  end
63
65
 
66
+ #
67
+ # it is a read-only result value, not an object, so it can't
68
+ # be the target of a verb or message. If the path refers to it,
69
+ # report that (what it can't be: 'sent messages', 'run', ...,
70
+ # and what to do instead) and return true.
71
+ #
72
+ def it_target_err?( what, hint = IT_HINT )
73
+ return false unless self.it?
74
+
75
+ @engine.err format( IT_NOT_AN_OBJECT, what, hint )
76
+ return true
77
+ end
78
+
64
79
  #
65
80
  # Does the pathname reference refer to error?
66
81
  #
@@ -141,19 +156,29 @@ module Gloo
141
156
  self.set_to( "#{@engine.heap.context}#{@src[1..-1]}" )
142
157
  end
143
158
 
159
+ #
160
+ # Expand any here (^) or context (@) reference into the
161
+ # full path, so the lookup below starts from the heap root.
162
+ #
163
+ def expand_refs
164
+ Here.expand_here( @engine, self ) if Here.includes_here_ref?( @elements )
165
+ expand_context if self.includes_context?
166
+ end
167
+
144
168
  #
145
169
  # Get the parent that contains the object referenced.
146
170
  #
171
+ # This is a lookup, not a use of the object: a missing element
172
+ # returns nil without reporting an error. A caller that needs the
173
+ # object reports its own error.
174
+ #
147
175
  def get_parent
148
176
  o = @engine.heap.root
149
177
 
150
178
  if self.includes_path?
151
179
  @elements[ 0..-2 ].each do |e|
152
180
  o = o.find_child( e )
153
- if o.nil?
154
- @engine.err "Object '#{e}' was not found."
155
- return nil
156
- end
181
+ return nil if o.nil?
157
182
  end
158
183
  end
159
184
 
@@ -162,12 +187,14 @@ module Gloo
162
187
 
163
188
  #
164
189
  # Does the object at the path exist?
190
+ # A question, so a missing object is an answer, not an error.
165
191
  #
166
192
  def exists?
167
193
  return true if self.root?
168
194
  return true if self.it?
169
195
  return true if self.error?
170
196
 
197
+ expand_refs
171
198
  parent = self.get_parent
172
199
  return false unless parent
173
200
 
@@ -186,6 +213,7 @@ module Gloo
186
213
  #
187
214
  # Resolve the pathname reference.
188
215
  # Find the object referenced or return nil if it is not found.
216
+ # Like get_parent, a missing object is not reported here.
189
217
  #
190
218
  def resolve
191
219
  return @engine.heap.root if self.root?
@@ -194,14 +222,7 @@ module Gloo
194
222
  return Gloo::Core::GlooSystem.new(
195
223
  @engine, self ) if self.gloo_sys?
196
224
 
197
- if Here.includes_here_ref?( @elements )
198
- Here.expand_here( @engine, self )
199
- end
200
-
201
- if self.includes_context?
202
- expand_context
203
- end
204
-
225
+ expand_refs
205
226
  parent = self.get_parent
206
227
  return nil unless parent
207
228
 
@@ -18,6 +18,10 @@ module Gloo
18
18
 
19
19
  QUOTE_CHARS = [ '"', "'" ].freeze
20
20
 
21
+ UNCLOSED_QUOTE_ERR = 'Unclosed quote.'.freeze
22
+ UNCLOSED_PAREN_ERR = 'Unclosed parenthesis.'.freeze
23
+ UNMATCHED_PAREN_ERR = "Unmatched ')'.".freeze
24
+
21
25
  attr_reader :cmd, :tokens
22
26
 
23
27
  # ---------------------------------------------------------------------
@@ -38,6 +42,40 @@ module Gloo
38
42
  # Public functions
39
43
  # ---------------------------------------------------------------------
40
44
 
45
+ #
46
+ # A syntax problem in the command (an unclosed quote, or
47
+ # parentheses that don't match), or nil. The tokens are still
48
+ # the best guess: an unclosed quote runs to the end of the line.
49
+ # Quotes are read the same way tokenize reads them.
50
+ #
51
+ def syntax_problem
52
+ quote = nil
53
+ depth = 0
54
+ escaped = false
55
+ @cmd.each_char do |ch|
56
+ if quote
57
+ if escaped
58
+ escaped = false
59
+ elsif ch == '\\'
60
+ escaped = true
61
+ elsif ch == quote
62
+ quote = nil
63
+ end
64
+ elsif QUOTE_CHARS.include?( ch )
65
+ quote = ch
66
+ elsif ch == '('
67
+ depth += 1
68
+ elsif ch == ')'
69
+ depth -= 1
70
+ return UNMATCHED_PAREN_ERR if depth.negative?
71
+ end
72
+ end
73
+ return UNCLOSED_QUOTE_ERR if quote
74
+ return UNCLOSED_PAREN_ERR if depth.positive?
75
+
76
+ return nil
77
+ end
78
+
41
79
  #
42
80
  # Get the number of tokens
43
81
  #
@@ -29,6 +29,60 @@ module Gloo
29
29
  Dictionary.instance.register_verb( subclass )
30
30
  end
31
31
 
32
+ #
33
+ # Report any syntax problem in the command's tokens or params
34
+ # (see Tokens#syntax_problem) as a syntax error. The verb still
35
+ # runs afterward, with the tokens' best guess.
36
+ #
37
+ def check_syntax
38
+ [ @tokens, @params ].each do |t|
39
+ problem = t.syntax_problem if t.respond_to?( :syntax_problem )
40
+ @engine.syntax_err problem if problem
41
+ end
42
+ end
43
+
44
+ #
45
+ # For a verb that takes no object: whatever was written after it,
46
+ # as one string (empty if nothing was).
47
+ #
48
+ def extra_words
49
+ words = @tokens.respond_to?( :params ) ? @tokens.params.to_a : []
50
+ words += @params.tokens.to_a if @params.respond_to?( :tokens )
51
+ return words.join( ' ' )
52
+ end
53
+
54
+ #
55
+ # The verb as written in the command (keyword or shortcut).
56
+ #
57
+ def written_verb
58
+ written = @tokens.verb if @tokens.respond_to?( :verb )
59
+ return written || self.class.keyword
60
+ end
61
+
62
+ #
63
+ # For a harmless verb that takes no object: warn if anything was
64
+ # written after it. The verb still runs.
65
+ #
66
+ def warn_extra_words
67
+ extra = extra_words
68
+ return if extra.empty?
69
+
70
+ @engine.warn "#{written_verb} takes no object; ignoring '#{extra}'."
71
+ end
72
+
73
+ #
74
+ # For a destructive verb that takes no object: if anything was
75
+ # written after it, report an error and return true; the verb
76
+ # should not run. The detail says what didn't happen and what to
77
+ # do instead.
78
+ #
79
+ def extra_words_err?( detail )
80
+ return false if extra_words.empty?
81
+
82
+ @engine.err "#{written_verb} takes no object, so #{detail}."
83
+ return true
84
+ end
85
+
32
86
  #
33
87
  # Run the verb.
34
88
  #
@@ -12,7 +12,6 @@ module Gloo
12
12
  module Exec
13
13
  class Dispatch
14
14
 
15
- OBJ_NOT_FOUND_ERR = 'Object was not found: '.freeze
16
15
 
17
16
  #
18
17
  # Send a message to an object of a given name (and path).
@@ -20,10 +19,14 @@ module Gloo
20
19
  #
21
20
  def self.send_message( engine, msg, to_obj_pn, params = nil )
22
21
  pn = Gloo::Core::Pn.new( engine, to_obj_pn )
22
+ return if pn.it_target_err?( 'sent messages' )
23
+
23
24
  target_obj = pn.resolve
24
25
 
25
26
  unless target_obj
26
- engine.err "#{OBJ_NOT_FOUND_ERR} #{to_obj_pn}"
27
+ # The message never ran, so there is no result.
28
+ engine.err Gloo::Core::NotFound.object( to_obj_pn )
29
+ engine.heap.it.set_to false
27
30
  return
28
31
  end
29
32
  Gloo::Exec::Dispatch.message( engine, msg, target_obj, params )
@@ -44,10 +47,6 @@ module Gloo
44
47
  # Dispatch an action.
45
48
  #
46
49
  def self.action( engine, action )
47
- unless action.valid?
48
- engine.log.warn "Object #{action.to.name} does not respond to #{action.msg}"
49
- end
50
-
51
50
  engine.exec_env.push_action action
52
51
  engine.log.debug "Sending message #{action.msg} to #{action.to.name}"
53
52
  action.dispatch
@@ -12,6 +12,9 @@ module Gloo
12
12
  attr_accessor :verbs, :actions, :scripts, :here
13
13
  attr_reader :running_script
14
14
 
15
+ # The file and line being loaded, if a file is being loaded.
16
+ attr_accessor :load_location
17
+
15
18
  VERB_STACK = 'verbs'.freeze
16
19
  ACTION_STACK = 'actions'.freeze
17
20
  SCRIPT_STACK = 'scripts'.freeze
@@ -31,6 +34,16 @@ module Gloo
31
34
  @here = Gloo::Exec::Stack.new( @engine, HERE_STACK )
32
35
  end
33
36
 
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.
40
+ #
41
+ def current_location
42
+ return @running_script.location if @running_script
43
+
44
+ return @load_location
45
+ end
46
+
34
47
  #
35
48
  # Get the here object.
36
49
  #
@@ -20,8 +20,12 @@ module Gloo
20
20
  engine.heap.error.start_tracking
21
21
  engine.exec_env.verbs.push verb
22
22
  begin
23
+ verb&.check_syntax
23
24
  verb&.run
24
- rescue => ex
25
+ rescue StandardError, ScriptError => ex
26
+ # ScriptError (a SyntaxError from eval'd code, a LoadError
27
+ # from a missing gem) isn't a StandardError, and would
28
+ # otherwise stop gloo altogether.
25
29
  engine.handle_exception( ex )
26
30
  ensure
27
31
  engine.exec_env.verbs.pop
@@ -36,12 +40,16 @@ module Gloo
36
40
  def self.run( engine, path_name )
37
41
  engine.log.debug "running script at #{path_name}"
38
42
  pn = Gloo::Core::Pn.new( engine, path_name )
43
+ return if pn.it_target_err?( 'run' )
44
+
39
45
  o = pn.resolve
40
46
 
41
47
  if o
42
48
  o.send_message 'run'
43
49
  else
44
- engine.err "Could not send message to object. Bad path: #{path_name}"
50
+ # The message never ran, so there is no result.
51
+ engine.err Gloo::Core::NotFound.object( path_name )
52
+ engine.heap.it.set_to false
45
53
  end
46
54
  end
47
55
 
@@ -16,6 +16,7 @@ module Gloo
16
16
  @engine = engine
17
17
  @obj = obj
18
18
  @break_out = false
19
+ @line = 0
19
20
  end
20
21
 
21
22
  #
@@ -27,10 +28,13 @@ module Gloo
27
28
  @engine.exec_env.push_script self
28
29
 
29
30
  if @obj.value.is_a? String
31
+ @line = 1
30
32
  @engine.parser.run @obj.value
31
33
  elsif @obj.value.is_a? Array
32
- @obj.value.each do |line|
34
+ @obj.value.each_with_index do |line, i|
33
35
  break if @break_out
36
+
37
+ @line = i + 1
34
38
  @engine.parser.run line
35
39
  end
36
40
  end
@@ -38,6 +42,14 @@ module Gloo
38
42
  @engine.exec_env.pop_script
39
43
  end
40
44
 
45
+ #
46
+ # Where this script is in its run: its path and the line now
47
+ # running (counting from 1), for error messages.
48
+ #
49
+ def location
50
+ return "#{@obj.pn}, line #{@line}"
51
+ end
52
+
41
53
  #
42
54
  # Generic function to get display value.
43
55
  # Can be used for debugging, etc.
@@ -8,6 +8,8 @@ module Gloo
8
8
  module Expr
9
9
  class Expression
10
10
 
11
+ DIVIDE_BY_ZERO_ERR = 'Division by zero.'.freeze
12
+
11
13
  # ---------------------------------------------------------------------
12
14
  # Constructor
13
15
  # ---------------------------------------------------------------------
@@ -36,6 +38,7 @@ module Gloo
36
38
 
37
39
  @symbols.each do |sym|
38
40
  if sym.is_a? Gloo::Core::Op
41
+ missing_value( @op ) if @op
39
42
  @op = sym
40
43
  elsif @left.nil?
41
44
  @left = sym
@@ -45,6 +48,7 @@ module Gloo
45
48
 
46
49
  perform_op if @left && @right
47
50
  end
51
+ missing_value( @op ) if @op
48
52
 
49
53
  return @left.value if @left.is_a? Gloo::Core::Literal
50
54
  return @left.value if @left.is_a? Gloo::Expr::Call
@@ -66,11 +70,39 @@ module Gloo
66
70
  @op ||= Gloo::Core::Op.default_op
67
71
  l = evaluate_sym @left
68
72
  r = evaluate_sym @right
69
- @left = @op.perform l, r
73
+ @left = apply_op( @op, l, r )
70
74
  @right = nil
71
75
  @op = nil
72
76
  end
73
77
 
78
+ #
79
+ # Apply the operator. An operator that can't be used with the value
80
+ # (eg. 'a' / 2), or a division by zero, is a runtime error, and the
81
+ # result is no value. A missing value (nil) was already reported.
82
+ #
83
+ def apply_op( op, left, right )
84
+ result = op.perform( left, right )
85
+ if op.is_a?( Gloo::Expr::OpDiv ) && result.is_a?( Float ) &&
86
+ ( result.nan? || result.infinite? )
87
+ raise ZeroDivisionError
88
+ end
89
+ return result unless result.nil? && !left.nil?
90
+
91
+ @engine.err "Operator '#{op.class::SYMBOL}' can't be used with '#{left}'."
92
+ return nil
93
+ rescue ZeroDivisionError
94
+ @engine.err DIVIDE_BY_ZERO_ERR
95
+ return nil
96
+ end
97
+
98
+ #
99
+ # An operator without a value on one side (eg. 'show 1 +') is a
100
+ # syntax error; the best guess is to leave the operator out.
101
+ #
102
+ def missing_value( op )
103
+ @engine.syntax_err "Operator '#{op.class::SYMBOL}' is missing a value."
104
+ end
105
+
74
106
  #
75
107
  # Evaluate the symbol and get a simple value.
76
108
  #
@@ -88,8 +120,14 @@ module Gloo
88
120
  def resolve_ref( ref )
89
121
  return ref.src if ref.named_color?
90
122
 
123
+ name = ref.src
91
124
  ob = ref.resolve
92
125
  return ob.value if ob
126
+
127
+ # The object is being used as a value, so a missing object is
128
+ # a runtime error (the lookup itself doesn't report it).
129
+ @engine.err Gloo::Core::NotFound.object( name )
130
+ return nil
93
131
  end
94
132
 
95
133
  #
@@ -9,6 +9,9 @@ module Gloo
9
9
  class OpPlus < Gloo::Core::Op
10
10
 
11
11
  SYMBOL = '+'.freeze
12
+ # An alternate spelling that reads better when joining strings:
13
+ # put first and ' ' and last into full_name
14
+ ALT_SYMBOL = 'and'.freeze
12
15
 
13
16
  #
14
17
  # Perform the operation and return the result.
@@ -12,6 +12,7 @@ module Gloo
12
12
  KEYWORD_SHORT = 'bool'.freeze
13
13
  TRUE = 'true'.freeze
14
14
  FALSE = 'false'.freeze
15
+ BOOLEAN_STRINGS = [ TRUE, FALSE, 't', 'f' ].freeze
15
16
 
16
17
  #
17
18
  # The name of the object type.
@@ -32,6 +33,20 @@ module Gloo
32
33
  #
33
34
  def set_value( new_value )
34
35
  self.value = Gloo::Objs::Boolean.coerse_to_bool( new_value )
36
+ return unless Gloo::Objs::Boolean.bad_value?( new_value )
37
+
38
+ @engine.warn "'#{new_value}' is not true or false; using #{self.value}."
39
+ end
40
+
41
+ #
42
+ # Is the value a string that isn't a boolean? A blank value is
43
+ # no value (false).
44
+ #
45
+ def self.bad_value?( new_value )
46
+ return false unless new_value.is_a?( ::String )
47
+ return false if new_value.strip.empty?
48
+
49
+ return !BOOLEAN_STRINGS.include?( new_value.strip.downcase )
35
50
  end
36
51
 
37
52
  #
@@ -132,6 +147,7 @@ module Gloo
132
147
  'true — Set the boolean to true.',
133
148
  'false — Set the boolean to false.'
134
149
  ],
150
+ :notes => 'Recognized values are true, false, t and f (in any case), and integers (0 is false). Any other string is false, with a warning: "\'maybe\' is not true or false; using false." A blank value is false.',
135
151
  :examples => <<~EXAMPLES.strip
136
152
  b [can] :
137
153
  flag [boolean] : true
@@ -10,6 +10,8 @@ module Gloo
10
10
 
11
11
  KEYWORD = 'container'.freeze
12
12
  KEYWORD_SHORT = 'can'.freeze
13
+ MISSING_INDEX_MSG = 'Missing index!'.freeze
14
+ MISSING_NAME_MSG = 'Missing child name!'.freeze
13
15
 
14
16
  #
15
17
  # The name of the object type.
@@ -33,7 +35,8 @@ module Gloo
33
35
  # Get a list of message names that this object receives.
34
36
  #
35
37
  def self.messages
36
- return super + %w[count delete_children child_exists show_key_value_table]
38
+ return super + %w[count delete_children child_exists show_key_value_table
39
+ child_value_at child_path_at random_child_value random_child_path]
37
40
  end
38
41
 
39
42
  #
@@ -65,17 +68,138 @@ module Gloo
65
68
  # Check to see if there is a child with the given name.
66
69
  #
67
70
  def msg_child_exists
68
- if @params&.token_count&.positive?
69
- expr = Gloo::Expr::Expression.new( @engine, @params.tokens )
70
- data = expr.evaluate
71
+ unless @params&.token_count&.positive?
72
+ @engine.syntax_err MISSING_NAME_MSG
73
+ return put_it( false )
71
74
  end
72
- return unless data
75
+
76
+ expr = Gloo::Expr::Expression.new( @engine, @params.tokens )
77
+ data = expr.evaluate
78
+ return put_it( false ) if data.nil?
73
79
 
74
80
  val = self.contains_child?( data )
75
81
  @engine.heap.it.set_to val
76
82
  return val
77
83
  end
78
84
 
85
+ #
86
+ # Get the value of the child at the given 0-based index.
87
+ #
88
+ def msg_child_value_at
89
+ child = child_at_param
90
+ return child if child == false
91
+
92
+ return put_child_value( child )
93
+ end
94
+
95
+ #
96
+ # Get the path of the child at the given 0-based index.
97
+ #
98
+ def msg_child_path_at
99
+ child = child_at_param
100
+ return child if child == false
101
+
102
+ return put_it( child.pn )
103
+ end
104
+
105
+ #
106
+ # Get the value of a random child.
107
+ #
108
+ def msg_random_child_value
109
+ child = random_child
110
+ return child if child == false
111
+
112
+ return put_child_value( child )
113
+ end
114
+
115
+ #
116
+ # Get the path of a random child.
117
+ #
118
+ def msg_random_child_path
119
+ child = random_child
120
+ return child if child == false
121
+
122
+ return put_it( child.pn )
123
+ end
124
+
125
+ # ---------------------------------------------------------------------
126
+ # Child Access Helpers
127
+ # ---------------------------------------------------------------------
128
+
129
+ #
130
+ # Find the child at the index given as the message parameter.
131
+ # Reports the problem, puts false into it, and returns false
132
+ # if there is no index or no child at that index.
133
+ #
134
+ def child_at_param
135
+ unless @params&.token_count&.positive?
136
+ @engine.syntax_err MISSING_INDEX_MSG
137
+ return put_it( false )
138
+ end
139
+
140
+ expr = Gloo::Expr::Expression.new( @engine, @params.tokens )
141
+ data = expr.evaluate
142
+ index = Container.to_index( data )
143
+ if index.nil?
144
+ @engine.err "Index '#{data}' is not a number!"
145
+ return put_it( false )
146
+ end
147
+
148
+ if index.negative? || index >= child_count
149
+ @engine.err "Index #{index} is out of range (0 to #{child_count - 1})!"
150
+ return put_it( false )
151
+ end
152
+
153
+ return children[ index ]
154
+ end
155
+
156
+ #
157
+ # Get a whole number index from an evaluated parameter.
158
+ # Returns nil if it isn't one.
159
+ #
160
+ def self.to_index( data )
161
+ return data if data.is_a?( ::Integer )
162
+ return data.strip.to_i if data.is_a?( ::String ) && data.strip.match?( /\A-?\d+\z/ )
163
+
164
+ return nil
165
+ end
166
+
167
+ #
168
+ # Pick a random child.
169
+ # Reports the problem, puts false into it, and returns false
170
+ # if the container is empty.
171
+ #
172
+ def random_child
173
+ if child_count.zero?
174
+ @engine.err "Container '#{pn}' is empty!"
175
+ return put_it( false )
176
+ end
177
+
178
+ return children.sample
179
+ end
180
+
181
+ #
182
+ # Put the value of the child into it.
183
+ # A container child has no value of its own; use its path instead.
184
+ #
185
+ def put_child_value( child )
186
+ child = Gloo::Objs::Alias.resolve_alias( @engine, child )
187
+ if child.is_container?
188
+ @engine.err "Child '#{child.pn}' is a container, not a value! Use its path instead."
189
+ return put_it( false )
190
+ end
191
+
192
+ return put_it( child.value )
193
+ end
194
+
195
+ #
196
+ # Put the value into it and return it.
197
+ #
198
+ def put_it( val )
199
+ @engine.heap.it.set_to val
200
+ return val
201
+ end
202
+
79
203
  # ---------------------------------------------------------------------
80
204
  # Object Documentation
81
205
  # ---------------------------------------------------------------------
@@ -99,8 +223,17 @@ module Gloo
99
223
  'count — Count the number of children objects in the container. The result is put in it.',
100
224
  'delete_children — Delete all children objects from the container.',
101
225
  'show_key_value_table — Show a table with key (name) and values for all children in the container.',
102
- 'child_exists ({name}) — Check to see if there is a child with the given name. A parameter is required. It will have a boolean.'
226
+ 'child_exists ({name}) — Check to see if there is a child with the given name. A parameter is required. It will have a boolean.',
227
+ 'child_value_at ({index}) — Get the value of the child at position {index}. Positions are 0-based and in the order the children were added, so after split_list, index 0 is the child named 1. A parameter is required. It will have the value; an out-of-range or non-numeric index, or a child that is a container, is an error, and it will have false.',
228
+ 'child_path_at ({index}) — Get the path (from root) of the child at position {index}, 0-based. Works for any child, including a container; put it into an alias (put it into ptr*) to reach the child and its fields. A parameter is required. It will have the path; an out-of-range or non-numeric index is an error, and it will have false.',
229
+ 'random_child_value — Get the value of a randomly chosen child. It will have the value; an empty container, or picking a child that is a container, is an error, and it will have false.',
230
+ 'random_child_path — Get the path (from root) of a randomly chosen child. It will have the path; an empty container is an error, and it will have false.'
103
231
  ],
232
+ :notes => 'The random_child messages pick with replacement: two ' \
233
+ 'calls can pick the same child. For distinct picks, pick a ' \
234
+ 'random index with an integer\'s randomize message, keep the ' \
235
+ 'indexes already used as children of another container, and ' \
236
+ 'check it with child_exists before using child_path_at.',
104
237
  :examples => <<~EXAMPLES.strip
105
238
  can [can] :
106
239
  data [can] :
@@ -109,6 +242,21 @@ module Gloo
109
242
  3 : three
110
243
  on_load [script] :
111
244
  tell can.data to show_key_value_table
245
+
246
+ #
247
+ # Reach a container child's fields through an alias.
248
+ #
249
+ books [can] :
250
+ list [can] :
251
+ a [can] :
252
+ title [string] : Walden
253
+ b [can] :
254
+ title [string] : Emma
255
+ ptr [alias] :
256
+ on_load [script] :
257
+ tell books.list to child_path_at (1)
258
+ put it into books.ptr*
259
+ show books.ptr.title
112
260
  EXAMPLES
113
261
  }
114
262
  end
@@ -109,6 +109,7 @@ module Gloo
109
109
  'an optional parameter is included, round to the precision specified.',
110
110
  'format ({fmt}) — With no parameter, adds comma separators to the whole part (e.g. 1000.5 -> 1,000.5). With a parameter, uses it as a sprintf-style format string (e.g. \'%.2f\'). It will have the formatted string.'
111
111
  ],
112
+ :notes => 'A value that isn\'t a number (eg. \'x\' or \'1,5\') is used as a best guess, with a warning: "\'1,5\' is not a decimal number; using 1.0." A blank value is 0.0.',
112
113
  :examples => <<~EXAMPLES.strip
113
114
  d [can] :
114
115
  x [decimal] : 100
@@ -140,6 +140,7 @@ module Gloo
140
140
  'range to 6 and add 1 to the result.',
141
141
  'format ({fmt}) — With no parameter, adds comma separators (e.g. 1000 -> 1,000). With a parameter, uses it as a sprintf-style format string (e.g. \'%05d\'). It will have the formatted string.'
142
142
  ],
143
+ :notes => 'A value that isn\'t a whole number (eg. \'x\', \'12abc\' or \'3.7\') is used as a best guess, with a warning: "\'x\' is not an integer; using 0." A blank value is 0.',
143
144
  :examples => <<~EXAMPLES.strip
144
145
  #
145
146
  # Integer object.