gloo 6.3.1 → 6.4.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +13 -0
  3. data/docs/language_objects.md +38 -0
  4. data/docs/language_scripting.md +41 -0
  5. data/docs/verbs.md +27 -2
  6. data/lib/VERSION +1 -1
  7. data/lib/VERSION_NOTES +6 -0
  8. data/lib/gloo/core/obj.rb +40 -1
  9. data/lib/gloo/core/parser.rb +30 -1
  10. data/lib/gloo/core/tokens.rb +44 -18
  11. data/lib/gloo/expr/l_string.rb +8 -4
  12. data/lib/gloo/objs/basic/script.rb +28 -2
  13. data/lib/gloo/objs/web/json.rb +4 -1
  14. data/lib/gloo/persist/comment_buffer.rb +58 -0
  15. data/lib/gloo/persist/declaration_ledger.rb +61 -0
  16. data/lib/gloo/persist/disc_mech.rb +15 -2
  17. data/lib/gloo/persist/file_loader.rb +201 -102
  18. data/lib/gloo/persist/file_saver.rb +251 -6
  19. data/lib/gloo/persist/file_storage.rb +10 -4
  20. data/lib/gloo/persist/indent_stack.rb +90 -0
  21. data/lib/gloo/persist/line_splitter.rb +18 -3
  22. data/lib/gloo/persist/persist_man.rb +159 -24
  23. data/lib/gloo/persist/script_body_collector.rb +102 -0
  24. data/lib/gloo/persist/shorthand_expander.rb +51 -0
  25. data/lib/gloo/persist/source/blank_node.rb +27 -0
  26. data/lib/gloo/persist/source/comment_node.rb +27 -0
  27. data/lib/gloo/persist/source/directive_node.rb +26 -0
  28. data/lib/gloo/persist/source/obj_node.rb +50 -0
  29. data/lib/gloo/persist/source/source_doc.rb +41 -0
  30. data/lib/gloo/verbs/load.rb +5 -0
  31. data/lib/gloo/verbs/reload.rb +3 -0
  32. data/lib/gloo/verbs/save.rb +57 -10
  33. data/test.gloo/lang/shorthand.test.gloo +29 -0
  34. data/test.gloo/objs/json.test.gloo +9 -0
  35. data/test.gloo/objs/script.test.gloo +1 -1
  36. data/test.gloo/string/str.test.gloo +15 -0
  37. data/test.gloo/verbs/reload.test.gloo +57 -0
  38. data/test.gloo/verbs/save.test.gloo +115 -0
  39. metadata +14 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 517b0f6358d83303a3e6e7536b9a16abd4a71cf9ef1649faf6abd21443fe652d
4
- data.tar.gz: b43125b452393abea79914d54a2f73e7d77791891ed3e14cde8ad51e2bdb44ce
3
+ metadata.gz: 345f1e1145a1c3e32a2d94f39eda0246c0b36c8a6aa36857cb8a459042b97b0a
4
+ data.tar.gz: a8cc80d6cfe82bd0f938cf9993e3948c7ae0ca1434fc430bb79a357239f8ac70
5
5
  SHA512:
6
- metadata.gz: cb9e73f9e4d4281d3ce190258bf744150176ac09c2f0e980b920f185a583015fdd3a5b64180fa0c07b6970f04f8c0eba49840495b35291846bd86e0e57dcf3d7
7
- data.tar.gz: ac5e8e247415a01107fee4cc12358559eafc698ac2e5c1ffdd5ee6cc01e6c7cca54d5b9eb44fdc589a8ce692fd36367103125571d5cff038a10b6cabe27ca4ac
6
+ metadata.gz: 1c6f947e4282de5e585543771f30d42ec3ab85197237e805d1c560c06fc286ecd5053b5627b86f7f017368dcf3ae957c1784f74b498e885feacc247a43b17ea1
7
+ data.tar.gz: cd6e0ad73493fd5dd7e56dff790ed22dafa31a32d2f2347fb263cb4d6177aef4e83aaab7127ad28899d30802ffdddd6b83415f57ae27f2fc8dc2001494cd86cf
data/CLAUDE.md CHANGED
@@ -58,6 +58,19 @@ Base classes: inherit from `GlooTest` (defined in `test/base_test.rb`).
58
58
  ### Gloo integration tests — `test.gloo/`
59
59
  Written in gloo itself. Each file contains `[test]` objects with `on_test` scripts using `assert` and `refute`.
60
60
 
61
+ Run with `ruby lib/run.rb --test --quiet <paths...>` — **not** `bundle exec` (the Gemfile doesn't carry `gloo-test` / `gloo-md` / the other core-lib gems, so `[test]` and `load lib` fail under bundler; plain `ruby` uses the system gem env where they're installed).
62
+
63
+ - Interpreter suite only:
64
+ `ruby ~/dev/gloo/lib/run.rb --test --quiet ~/dev/gloo/test.gloo/`
65
+ - Interpreter **plus** every extension and core library:
66
+ ```
67
+ ruby ~/dev/gloo/lib/run.rb --test --quiet \
68
+ ~/dev/gloo/test.gloo/ \
69
+ ~/gloo/extensions/{alert,beep,git,ruby,slack,stats,teams}/test/ \
70
+ ~/dev/gloo_core_libraries/gloo-{beep,cli,db,email,md,mysql,pg,sqlite,test,web,yaml}/test/
71
+ ```
72
+ (brace expansion is shell sugar for the full explicit path list)
73
+
61
74
  ```
62
75
  test.gloo/
63
76
  ├── basic.test.gloo
@@ -3,6 +3,7 @@
3
3
  **Contents**
4
4
 
5
5
  - Object Naming
6
+ - Nested Container Shorthand
6
7
  - Keywords
7
8
  - Literals
8
9
  - Value Conversion
@@ -39,6 +40,36 @@ naming [container] :
39
40
 
40
41
  See also: Pathname.
41
42
 
43
+ ## Nested Container Shorthand
44
+
45
+ A declaration whose name is a dotted path creates a container for each
46
+ prefix segment, then declares the real object under the last one. These
47
+ two files are equivalent:
48
+
49
+ ```gloo
50
+ page [container] :
51
+ core [container] :
52
+ users [container] :
53
+ list [container] :
54
+ title [string] : Users
55
+ ```
56
+
57
+ ```gloo
58
+ page.core.users.list [container] :
59
+ title [string] : Users
60
+ ```
61
+
62
+ - Each prefix segment (`page`, `core`, `users`) becomes a container,
63
+ unless a sibling object of that name already exists — in which case
64
+ that object is used as-is.
65
+ - The shorthand works at any indent level; the prefix is resolved
66
+ relative to the current parent.
67
+ - Several shorthand lines can share a prefix — `page.core.users [...]`
68
+ and `page.core.settings [...]` both reuse the same `page` and
69
+ `page.core` containers.
70
+ - Indented lines below a shorthand declaration are children of the last
71
+ segment (`list` above), not of any prefix container.
72
+
42
73
  ## Keywords
43
74
 
44
75
  Gloo doesn't reserve words the way many languages do. A verb keyword like `put` or an object type name like `string` can also be used as an object name — there's no parser conflict, because verbs are only looked up as the first word of a statement, and object type names are only looked up where a type is expected (inside the `[ ]` on a declaration). Everywhere else, the word is just a pathname segment (see Object Naming, above).
@@ -60,6 +91,11 @@ The following rules apply to literal values:
60
91
 
61
92
  - Strings
62
93
  - Can be delimited by single or double quotes. (`"` or `'`)
94
+ - A quote of the other kind inside needs no escaping —
95
+ `'{"x":1}'` is the string `{"x":1}`, which is the usual way to
96
+ write a JSON literal.
97
+ - A quote of the same kind inside is escaped with a backslash —
98
+ `"say \"hi\""` is the string `say "hi"`.
63
99
  - Numbers
64
100
  - Integer and decimal numbers need no delimiters.
65
101
  - To refer to a decimal with no fractional value, include `.0` to indicate a decimal value.
@@ -89,6 +125,8 @@ The following rules apply to literal values:
89
125
  show ^.s
90
126
  put "You're Awesome!" into ^.s
91
127
  show ^.s
128
+ put '{"lang":"gloo"}' into ^.s
129
+ show ^.s
92
130
 
93
131
  # Number literals
94
132
  put 1 into ^.i
@@ -3,6 +3,7 @@
3
3
  **Contents**
4
4
 
5
5
  - Gloo Script Files
6
+ - Comments
6
7
  - Gloo Constants
7
8
  - Line Continuation
8
9
 
@@ -26,6 +27,46 @@ hello [can] :
26
27
  on_load [script] : show 'hello world'
27
28
  ```
28
29
 
30
+ ## Comments
31
+
32
+ A line whose first non-blank character is `#` is a comment. Whole-line comments
33
+ can appear anywhere — between objects, inside a container, or inside a script
34
+ body — and are ignored when the file runs.
35
+
36
+ ```gloo
37
+ #
38
+ # A whole-line comment.
39
+ #
40
+ demo [can] :
41
+ # a comment inside the container
42
+ msg [string] : hello
43
+ on_load [script] :
44
+ # a comment inside the script body
45
+ show demo.msg
46
+ ```
47
+
48
+ A statement line may also end with an inline `# ...` comment. It is stripped
49
+ before the statement runs, so it has no effect on execution:
50
+
51
+ ```gloo
52
+ on_load [script] :
53
+ show 3 + 4 # prints 7
54
+ check demo.msg for blank?
55
+ show it # prints false
56
+ ```
57
+
58
+ Two things are *not* treated as inline comments, and are left alone:
59
+
60
+ - a `#` inside a quoted string — `show 'a # b'`
61
+ - a `#` with no space before it — a URL fragment such as
62
+ `http://example.com/page#section`
63
+
64
+ An inline comment on an object declaration is also left alone — everything after
65
+ the `:` is the object's value, so `note [string] : see item # 5` stores the
66
+ string `see item # 5`.
67
+
68
+ ---
69
+
29
70
  ## Gloo Constants
30
71
 
31
72
  There is no gloo language construct for constants. They are simply objects. But by convention, constants are named in all caps. They might be in a container or at the root object level.
data/docs/verbs.md CHANGED
@@ -9,8 +9,9 @@ Verbs aren't just for scripts, though. They're also the interactive language of
9
9
  - Run
10
10
  - Tell
11
11
  - Put
12
+ - Load & Save
12
13
 
13
- This page walks through three of the most commonly used verbs to get a feel for how they work together. For the complete list of verbs, their full syntax, and every error they can raise, use the in-app help: enter `help` (or `?`), then `verbs` to list them all, or `verb {name}` for detail on one (see Application, Help).
14
+ This page walks through the most commonly used verbs to get a feel for how they work together. For the complete list of verbs, their full syntax, and every error they can raise, use the in-app help: enter `help` (or `?`), then `verbs` to list them all, or `verb {name}` for detail on one (see Application, Help).
14
15
 
15
16
  ## Run
16
17
 
@@ -67,6 +68,30 @@ put {expression} into {dst.path}
67
68
 
68
69
  `it` also picks up the result of the evaluation, same as with other verbs — see It.
69
70
 
71
+ ## Load & Save
72
+
73
+ `load` reads a `.gloo` file into the heap and runs its `on_load` script. Give a path relative to the project folder (no extension needed) or a full path (extension required); `*` in place of a file name loads every `.gloo` file in a folder.
74
+
75
+ ```gloo
76
+ > load my/project/config
77
+ > load my/app/*
78
+ > load ~/.my_app/settings.gloo
79
+ ```
80
+
81
+ `save` writes loaded objects back to their files. With no argument it saves every open file; with an object it saves the file (or files) that object's tree came from; with `to {path}` it saves to a new file and remembers the mapping.
82
+
83
+ ```gloo
84
+ > save # every open file
85
+ > save config # just config's file
86
+ > save config to backups/config
87
+ ```
88
+
89
+ A save is a **rewrite, not a regeneration**: comments, blank lines, and the original spacing are kept, and only the values you actually changed are re-written. A declaration you never touched comes back byte-for-byte.
90
+
91
+ Several files can contribute to one container — declare `app [container] :` in each and add different children. They merge in the heap, and each file's save only rewrites its own declarations. If two files declare the *same* object with different values, the first one loaded wins and `load` logs a warning.
92
+
93
+ An object can also save itself: `tell config to save`.
94
+
70
95
  ---
71
96
 
72
- `run`, `tell`, and `put` cover a lot of ground on their own, but there are 26 more verbs — `show`, `if`, `create`, `each`, `check`, and so on — all documented in-app. Enter `help` (or `?`), then `verbs` to browse them. (This page itself is also viewable in-app: `help> doc verbs`.)
97
+ `run`, `tell`, `put`, and `load` / `save` cover a lot of ground, but there are more than two dozen other verbs — `show`, `if`, `create`, `each`, `check`, `reload`, `unload`, and so on — all documented in-app. Enter `help` (or `?`), then `verbs` to browse them. (This page itself is also viewable in-app: `help> doc verbs`.)
data/lib/VERSION CHANGED
@@ -1 +1 @@
1
- 6.3.1
1
+ 6.4.0
data/lib/VERSION_NOTES CHANGED
@@ -1,3 +1,9 @@
1
+ 6.4.0 - 2026.09.06
2
+ - Persistance refresh with updates for loading and saving
3
+ - Nested container shortcut syntax
4
+ - Fixes a number of comment and quoting issues
5
+
6
+
1
7
  6.3.1 - 2026.09.01
2
8
  - Fixes issue with MD in a text block (# ignored as comments)
3
9
 
data/lib/gloo/core/obj.rb CHANGED
@@ -118,6 +118,29 @@ module Gloo
118
118
  return false
119
119
  end
120
120
 
121
+ #
122
+ # Serialize this object's value for saving to a file.
123
+ # Returns the text that follows "name [type]" on the declaration
124
+ # line -- including the leading ' :' and, for a multi-line string
125
+ # value, the additional indented lines and the closing END marker.
126
+ # indent is the indentation level of the declaration line itself.
127
+ #
128
+ def serialize_value( indent )
129
+ return " : #{value_display}" unless value_string? && value.include?( "\n" )
130
+
131
+ return serialize_block_value( indent )
132
+ end
133
+
134
+ #
135
+ # Serialize a multi-line string value as a BEGIN/END block.
136
+ #
137
+ def serialize_block_value( indent )
138
+ t = "\t" * indent
139
+ inner = "\t" * ( indent + 1 )
140
+ body = value.split( "\n" ).map { |line| "#{inner}#{line}" }.join( "\n" )
141
+ return " : BEGIN\n#{body}\n#{t}END"
142
+ end
143
+
121
144
  #
122
145
  # Is the value a String?
123
146
  #
@@ -306,7 +329,7 @@ module Gloo
306
329
  # Get a list of message names that this object receives.
307
330
  #
308
331
  def self.messages
309
- return %w[reload unload blank? contains? responds_to?]
332
+ return %w[save reload unload blank? contains? responds_to?]
310
333
  end
311
334
 
312
335
  #
@@ -356,6 +379,22 @@ module Gloo
356
379
  @engine.persist_man.unload self
357
380
  end
358
381
 
382
+ #
383
+ # Send the object the save message: tell it to save itself,
384
+ # rather than going through the save verb. Same rules as
385
+ # `save {path.to.object}` -- saves every file that owns a
386
+ # declaration in this object's root, or saves fresh to a default
387
+ # path if it isn't mapped to a file yet.
388
+ #
389
+ def msg_save
390
+ if self.root?
391
+ @engine.err 'Cannot save the root object.'
392
+ return
393
+ end
394
+
395
+ @engine.persist_man.save self.pn
396
+ end
397
+
359
398
  #
360
399
  # Send the object the reload message.
361
400
  # Note that this will only work for objects with file assoications.
@@ -21,7 +21,11 @@ module Gloo
21
21
  # Parse a command from the immediate execution context.
22
22
  #
23
23
  def parse_immediate( full_cmd )
24
- # Break the full command into verb and params
24
+ # Drop an inline trailing comment first, then break the
25
+ # command into verb and params.
26
+ full_cmd = strip_comment( full_cmd )
27
+ return nil if full_cmd.strip.empty?
28
+
25
29
  cmd, params = split_params full_cmd
26
30
 
27
31
  # Params are the parenthetical part of the command at the end
@@ -35,6 +39,31 @@ module Gloo
35
39
  return nil
36
40
  end
37
41
 
42
+ #
43
+ # Remove an inline trailing comment: the first '#' that starts a
44
+ # word (at the start of the command, or right after whitespace)
45
+ # and is not inside a quoted string -- and everything after it.
46
+ #
47
+ # Left alone: a '#' inside single or double quotes (show 'a # b'),
48
+ # a '#' glued to preceding text (a URL fragment like
49
+ # http://x?id=1#frag), and a command with no such '#'. Object
50
+ # declaration values in a .gloo file never reach here -- they're
51
+ # split by LineSplitter, not the parser.
52
+ #
53
+ def strip_comment( full_cmd )
54
+ quote = nil
55
+ full_cmd.each_char.with_index do |ch, i|
56
+ if quote
57
+ quote = nil if ch == quote
58
+ elsif Gloo::Core::Tokens::QUOTE_CHARS.include?( ch )
59
+ quote = ch
60
+ elsif ch == '#' && ( i.zero? || full_cmd[ i - 1 ] =~ /\s/ )
61
+ return full_cmd[ 0...i ].rstrip
62
+ end
63
+ end
64
+ return full_cmd
65
+ end
66
+
38
67
  #
39
68
  # If additional params were provided, split them out
40
69
  # from the token list.
@@ -154,8 +154,9 @@ module Gloo
154
154
  # An inline call (invoke( ... ) / ~>( ... )) is checked for
155
155
  # first since it needs to be quote-aware in its own right (a
156
156
  # call's args can include a quoted string) - see
157
- # find_call_range. Falls through to the original quote-then-
158
- # plain-split handling, unchanged, when there's no call.
157
+ # find_call_range. Otherwise the first quoted run (whichever
158
+ # quote char opens first) becomes one token, and the rest is
159
+ # split on spaces.
159
160
  #
160
161
  def tokenize( str )
161
162
  range = find_call_range( str )
@@ -163,27 +164,52 @@ module Gloo
163
164
  tokenize( str[ 0...range.first ] ) if range.first.positive?
164
165
  @tokens << str[ range ]
165
166
  tokenize( str[ range.last + 1..-1 ] ) if range.last + 1 < str.length
166
- elsif str.index( '"' )
167
- i = str.index( '"' )
168
- j = str.index( '"', i + 1 )
169
- j ||= str.length
170
-
171
- tokenize( str[ 0..i - 1 ] ) if i > 1
172
- @tokens << str[ i..j ]
173
- tokenize( str[ j + 1..-1 ] ) if j + 1 < str.length
174
- elsif str.index( "'" )
175
- i = str.index( "'" )
176
- j = str.index( "'", i + 1 )
177
- j ||= str.length
178
-
179
- tokenize( str[ 0..i - 1 ] ) if i > 1
180
- @tokens << str[ i..j ]
181
- tokenize( str[ j + 1..-1 ] ) if j + 1 < str.length
167
+ return
168
+ end
169
+
170
+ qi, qc = first_quote( str )
171
+ if qi
172
+ close = closing_quote( str, qi, qc )
173
+ tokenize( str[ 0...qi ] ) if qi.positive?
174
+ @tokens << str[ qi..close ]
175
+ tokenize( str[ close + 1..-1 ] ) if close + 1 < str.length
182
176
  else
183
177
  str.strip.split( ' ' ).each { |t| @tokens << t }
184
178
  end
185
179
  end
186
180
 
181
+ #
182
+ # Find the first quote character in the string, of either kind
183
+ # -- returns [index, char], or [nil, nil] if there is none. The
184
+ # kind that opens first wins, so a " inside a '...' literal (and
185
+ # vice versa) is treated as ordinary content.
186
+ #
187
+ def first_quote( str )
188
+ found = nil
189
+ QUOTE_CHARS.each do |q|
190
+ i = str.index( q )
191
+ found = [ i, q ] if i && ( found.nil? || i < found[ 0 ] )
192
+ end
193
+ return found || [ nil, nil ]
194
+ end
195
+
196
+ #
197
+ # Index of the quote that closes the one opened at open_index.
198
+ # A backslash-escaped quote (\" or \') does not close the
199
+ # string. Returns str.length if it is never closed.
200
+ #
201
+ def closing_quote( str, open_index, quote_char )
202
+ i = open_index + 1
203
+ while i < str.length
204
+ ch = str[ i ]
205
+ return i if ch == quote_char
206
+
207
+ i += 1
208
+ i += 1 if ch == '\\'
209
+ end
210
+ return str.length
211
+ end
212
+
187
213
  #
188
214
  # Find the char range of the first top-level (not inside a
189
215
  # quoted string) invoke(...)/~>(...) call in str. Returns nil
@@ -31,19 +31,23 @@ module Gloo
31
31
  end
32
32
 
33
33
  #
34
- # Given a string with leading and trailing quotes,
35
- # strip them out.
34
+ # Given a string with leading and trailing quotes, strip them
35
+ # out. A quote of the other kind inside needs nothing done to it
36
+ # ('{"x":1}' -> {"x":1}); an escaped quote of the same kind is
37
+ # unescaped ("say \"hi\"" -> say "hi").
36
38
  #
37
39
  def self.strip_quotes( str )
38
40
  if str.start_with?( '"' )
39
41
  str = str[ 1..-1 ]
40
42
  str = str[ 0..-2 ] if str.end_with?( '"' )
41
- return str
43
+ return str.gsub( '\\"', '"' )
42
44
  elsif str.start_with?( "'" )
43
45
  str = str[ 1..-1 ]
44
46
  str = str[ 0..-2 ] if str.end_with?( "'" )
45
- return str
47
+ return str.gsub( "\\'", "'" )
46
48
  end
49
+
50
+ return str
47
51
  end
48
52
 
49
53
  #
@@ -47,7 +47,7 @@ module Gloo
47
47
  if self.value_string?
48
48
  first = self.value
49
49
  self.set_array_value []
50
- self.value << first unless first.empty?
50
+ self.value << first unless first.strip.empty?
51
51
  elsif self.value_is_blank?
52
52
  self.set_array_value []
53
53
  end
@@ -62,6 +62,29 @@ module Gloo
62
62
  return true
63
63
  end
64
64
 
65
+ #
66
+ # Serialize this object's value for saving to a file.
67
+ # A script's value is an Array of command lines (once add_line
68
+ # has been called) or a single-line String. Render as indented
69
+ # body lines below the declaration, gloo's convention for
70
+ # scripts -- not a BEGIN/END block, which is reserved for a
71
+ # literal multi-line string value.
72
+ #
73
+ def serialize_value( indent )
74
+ lines = if value_is_array?
75
+ value
76
+ elsif value_string? && !value.strip.empty?
77
+ [ value ]
78
+ else
79
+ []
80
+ end
81
+ return ' :' if lines.empty?
82
+
83
+ t = "\t" * ( indent + 1 )
84
+ body = lines.map { |line| "#{t}#{line}" }.join( "\n" )
85
+ return " :\n#{body}"
86
+ end
87
+
65
88
  #
66
89
  # Get the number of lines in this script.
67
90
  #
@@ -105,7 +128,10 @@ module Gloo
105
128
  {
106
129
  :name => KEYWORD,
107
130
  :shortcut => KEYWORD_SHORT,
108
- :description => 'An executable script — a set of commands to be run.',
131
+ :description => 'An executable script — a set of commands to ' \
132
+ 'be run. A command line may end with an inline `# ...` ' \
133
+ 'comment; it is ignored when the line runs but kept when ' \
134
+ 'the file is saved.',
109
135
  :messages => [
110
136
  'run — Run the script. The script can be run by telling the ' \
111
137
  'object to run, or via the run verb.'
@@ -213,7 +213,10 @@ module Gloo
213
213
  {
214
214
  :name => KEYWORD,
215
215
  :shortcut => KEYWORD_SHORT,
216
- :description => 'JSON data in a text string.',
216
+ :description => 'JSON data in a text string. Declare it ' \
217
+ 'inline, in a BEGIN/END block, or assign it at run time: a ' \
218
+ 'single-quoted literal keeps its double quotes, so ' \
219
+ 'putting a JSON string into the object works.',
217
220
  :messages => [
218
221
  '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.',
219
222
  '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.',
@@ -0,0 +1,58 @@
1
+ # Author:: Eric Crane (mailto:eric.crane@mac.com)
2
+ # Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
3
+ #
4
+ # Buffers a run of consecutive whole-line comments while the loader
5
+ # decides whether they become a declaration's leading_doc or float as
6
+ # their own comment nodes.
7
+ #
8
+
9
+ module Gloo
10
+ module Persist
11
+ class CommentBuffer
12
+
13
+ #
14
+ # Set up an empty buffer.
15
+ #
16
+ def initialize
17
+ @pending = []
18
+ end
19
+
20
+ #
21
+ # Buffer one comment line. raw is the full source line (minus
22
+ # its trailing newline); tabs is its indentation level.
23
+ #
24
+ def push( raw, tabs )
25
+ @pending << { :raw => raw, :tabs => tabs }
26
+ end
27
+
28
+ #
29
+ # Detach the buffered run as floating comment nodes, appended in
30
+ # order to the given children array.
31
+ #
32
+ def flush_into( children )
33
+ return if @pending.empty?
34
+
35
+ @pending.each { |c| children << Source::CommentNode.new( c[ :raw ] ) }
36
+ @pending = []
37
+ end
38
+
39
+ #
40
+ # If the buffered run sits at the same indent as line_tabs, claim
41
+ # it as a leading_doc (raw, newline-joined) and clear the buffer.
42
+ # Otherwise flush it into children as floating comments first, so
43
+ # document order is kept, and return nil.
44
+ #
45
+ def take_leading_doc( line_tabs, children )
46
+ if !@pending.empty? && @pending.last[ :tabs ] == line_tabs
47
+ doc = @pending.map { |c| c[ :raw ] }.join( "\n" )
48
+ @pending = []
49
+ return doc
50
+ end
51
+
52
+ flush_into( children )
53
+ return nil
54
+ end
55
+
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,61 @@
1
+ # Author:: Eric Crane (mailto:eric.crane@mac.com)
2
+ # Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
3
+ #
4
+ # Tracks what a single file declares as it loads: its top-level
5
+ # objects (for save resolution), and every object it created -- so a
6
+ # re-declaration of a name an earlier file already used can be
7
+ # flagged, since in that case the second value is silently dropped.
8
+ #
9
+
10
+ module Gloo
11
+ module Persist
12
+ class DeclarationLedger
13
+
14
+ attr_reader :roots
15
+
16
+ #
17
+ # Set up a ledger for the file at pn.
18
+ #
19
+ def initialize( engine, pn )
20
+ @engine = engine
21
+ @pn = pn
22
+ @roots = []
23
+ @mine = {}.compare_by_identity
24
+ end
25
+
26
+ #
27
+ # Record a top-level object this file created or reused (shorthand
28
+ # lines can re-introduce the same root, so dedupe).
29
+ #
30
+ def root( obj )
31
+ @roots << obj unless @roots.include?( obj )
32
+ end
33
+
34
+ #
35
+ # Record an object this file created, at any level.
36
+ #
37
+ def created( obj )
38
+ @mine[ obj ] = true
39
+ end
40
+
41
+ #
42
+ # `prior` already existed when this file declared `name` with
43
+ # `value`. If another file created it and the values differ, the
44
+ # new value is being ignored -- warn so it isn't a surprise. A
45
+ # duplicate name inside one file is left alone (a documented
46
+ # "first one wins").
47
+ #
48
+ def clash( prior, name, value )
49
+ return if @mine.key?( prior )
50
+ return if value.nil? || value.to_s.strip.empty?
51
+ return if prior.value.to_s == value.to_s
52
+
53
+ @engine.log.warn(
54
+ "#{@pn}: '#{name}' is already declared in another loaded file with a " \
55
+ "different value (#{prior.value.inspect} kept, #{value.inspect} ignored)."
56
+ )
57
+ end
58
+
59
+ end
60
+ end
61
+ end
@@ -85,13 +85,26 @@ module Gloo
85
85
  return nil
86
86
  end
87
87
 
88
- #
88
+ #
89
89
  # Read in the contents of a single file.
90
- #
90
+ #
91
91
  def read( file )
92
92
  return File.read( file )
93
93
  end
94
94
 
95
+ #
96
+ # Resolve a path to save a new (not-yet-existing) file to, given
97
+ # a name or relative path -- same convention as expand (relative
98
+ # to the project root, .gloo appended if missing), but without
99
+ # requiring the file to already exist.
100
+ #
101
+ def resolve_save_path( name )
102
+ full_name = name.end_with?( file_ext ) ? name : "#{name}#{file_ext}"
103
+ return full_name if full_name.start_with?( '/' )
104
+
105
+ return File.join( @engine.settings.project_path, full_name )
106
+ end
107
+
95
108
  #
96
109
  # Write data to the file.
97
110
  #