gloo 6.3.1 → 6.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CLAUDE.md +13 -0
- data/docs/application.md +9 -0
- data/docs/language_objects.md +78 -0
- data/docs/language_scripting.md +41 -0
- data/docs/objects.md +1 -1
- data/docs/verbs.md +36 -2
- data/lib/VERSION +1 -1
- data/lib/VERSION_NOTES +13 -0
- data/lib/gloo/app/engine.rb +5 -1
- data/lib/gloo/app/log.rb +5 -5
- data/lib/gloo/app/settings.rb +4 -1
- data/lib/gloo/core/obj.rb +60 -1
- data/lib/gloo/core/parser.rb +30 -1
- data/lib/gloo/core/tokens.rb +44 -18
- data/lib/gloo/expr/l_string.rb +8 -4
- data/lib/gloo/objs/basic/script.rb +28 -2
- data/lib/gloo/objs/web/json.rb +4 -1
- data/lib/gloo/persist/comment_buffer.rb +58 -0
- data/lib/gloo/persist/declaration_ledger.rb +61 -0
- data/lib/gloo/persist/disc_mech.rb +15 -2
- data/lib/gloo/persist/file_loader.rb +208 -102
- data/lib/gloo/persist/file_saver.rb +251 -6
- data/lib/gloo/persist/file_storage.rb +10 -4
- data/lib/gloo/persist/indent_stack.rb +90 -0
- data/lib/gloo/persist/line_splitter.rb +18 -3
- data/lib/gloo/persist/persist_man.rb +193 -26
- data/lib/gloo/persist/script_body_collector.rb +102 -0
- data/lib/gloo/persist/shorthand_expander.rb +51 -0
- data/lib/gloo/persist/source/blank_node.rb +27 -0
- data/lib/gloo/persist/source/comment_node.rb +27 -0
- data/lib/gloo/persist/source/directive_node.rb +26 -0
- data/lib/gloo/persist/source/obj_node.rb +91 -0
- data/lib/gloo/persist/source/source_doc.rb +41 -0
- data/lib/gloo/verbs/list.rb +24 -2
- data/lib/gloo/verbs/load.rb +9 -1
- data/lib/gloo/verbs/reload.rb +3 -0
- data/lib/gloo/verbs/save.rb +57 -10
- data/test.gloo/ctrl/each.test.gloo +28 -30
- data/test.gloo/dt/date.test.gloo +69 -71
- data/test.gloo/dt/datetime.test.gloo +55 -57
- data/test.gloo/dt/time.test.gloo +70 -72
- data/test.gloo/lang/continuation.test.gloo +25 -27
- data/test.gloo/lang/convert.test.gloo +50 -52
- data/test.gloo/lang/dt_comparisons.test.gloo +87 -89
- data/test.gloo/lang/exceptions.test.gloo +42 -44
- data/test.gloo/lang/gloo_sys.test.gloo +100 -102
- data/test.gloo/lang/here.test.gloo +32 -34
- data/test.gloo/lang/it.test.gloo +27 -29
- data/test.gloo/lang/literal.test.gloo +55 -57
- data/test.gloo/lang/load_lib_directive.test.gloo +18 -20
- data/test.gloo/lang/naming.test.gloo +28 -30
- data/test.gloo/lang/ops.test.gloo +53 -55
- data/test.gloo/lang/shorthand.test.gloo +27 -0
- data/test.gloo/math/add.test.gloo +40 -42
- data/test.gloo/math/div.test.gloo +23 -25
- data/test.gloo/math/mult.test.gloo +23 -25
- data/test.gloo/math/sub.test.gloo +23 -25
- data/test.gloo/objs/alias.test.gloo +15 -17
- data/test.gloo/objs/bool.test.gloo +54 -56
- data/test.gloo/objs/can.test.gloo +33 -35
- data/test.gloo/objs/cipher.test.gloo +39 -41
- data/test.gloo/objs/decimal.test.gloo +27 -29
- data/test.gloo/objs/erb.test.gloo +47 -49
- data/test.gloo/objs/file.test.gloo +68 -70
- data/test.gloo/objs/function.test.gloo +37 -39
- data/test.gloo/objs/int.test.gloo +54 -56
- data/test.gloo/objs/json.test.gloo +57 -50
- data/test.gloo/objs/obj.test.gloo +68 -48
- data/test.gloo/objs/outline.test.gloo +48 -50
- data/test.gloo/objs/password.test.gloo +34 -36
- data/test.gloo/objs/repeat.test.gloo +42 -44
- data/test.gloo/objs/script.test.gloo +13 -15
- data/test.gloo/objs/string.test.gloo +82 -84
- data/test.gloo/objs/text.test.gloo +90 -92
- data/test.gloo/objs/untyped.test.gloo +36 -38
- data/test.gloo/objs/uri.test.gloo +37 -39
- data/test.gloo/string/str.test.gloo +15 -0
- data/test.gloo/verbs/break.test.gloo +12 -14
- data/test.gloo/verbs/check.test.gloo +22 -24
- data/test.gloo/verbs/context.test.gloo +11 -13
- data/test.gloo/verbs/create.test.gloo +7 -9
- data/test.gloo/verbs/eval.test.gloo +10 -12
- data/test.gloo/verbs/exists.test.gloo +43 -45
- data/test.gloo/verbs/if.test.gloo +8 -10
- data/test.gloo/verbs/invoke.test.gloo +73 -75
- data/test.gloo/verbs/list.test.gloo +21 -23
- data/test.gloo/verbs/load.test.gloo +27 -21
- data/test.gloo/verbs/log.test.gloo +7 -9
- data/test.gloo/verbs/move.test.gloo +12 -14
- data/test.gloo/verbs/put.test.gloo +10 -12
- data/test.gloo/verbs/reload.test.gloo +55 -0
- data/test.gloo/verbs/run.test.gloo +8 -10
- data/test.gloo/verbs/save.test.gloo +111 -0
- data/test.gloo/verbs/show.test.gloo +26 -28
- data/test.gloo/verbs/tell.test.gloo +8 -10
- data/test.gloo/verbs/throw.test.gloo +31 -33
- data/test.gloo/verbs/unless.test.gloo +13 -15
- data/test.gloo/verbs/unload.test.gloo +10 -12
- metadata +14 -1
|
@@ -12,6 +12,11 @@ module Gloo
|
|
|
12
12
|
|
|
13
13
|
attr_reader :maps, :mech
|
|
14
14
|
|
|
15
|
+
OBJ_NOT_FOUND_ERR = 'Could not resolve object to save: '.freeze
|
|
16
|
+
PATH_EXISTS_ERR = 'Will not overwrite a file not already saved there: '.freeze
|
|
17
|
+
RELOAD_DIRTY_WARNING = 'Reloading will discard unsaved changes in: '.freeze
|
|
18
|
+
FILE_NOT_FOUND_ERR = 'File not found: '.freeze
|
|
19
|
+
|
|
15
20
|
#
|
|
16
21
|
# Constructor for the persistence manager.
|
|
17
22
|
#
|
|
@@ -22,51 +27,84 @@ module Gloo
|
|
|
22
27
|
end
|
|
23
28
|
|
|
24
29
|
#
|
|
25
|
-
# Save one object to the file
|
|
30
|
+
# Save one object to the file, or every open file if no name is
|
|
31
|
+
# given.
|
|
26
32
|
#
|
|
27
33
|
def save( name = '' )
|
|
28
34
|
name.blank? ? save_all : save_one( name )
|
|
29
35
|
end
|
|
30
36
|
|
|
31
37
|
#
|
|
32
|
-
# Save
|
|
38
|
+
# Save every open file.
|
|
33
39
|
#
|
|
34
40
|
def save_all
|
|
35
|
-
@maps
|
|
41
|
+
save_batch( @maps )
|
|
36
42
|
end
|
|
37
43
|
|
|
38
44
|
#
|
|
39
|
-
# Save
|
|
45
|
+
# Save the given object: every file that owns a declaration in
|
|
46
|
+
# its root's tree (usually one -- more when the root is a
|
|
47
|
+
# multi-file namespace). If it isn't mapped to any file yet, it's
|
|
48
|
+
# saved fresh to a default path derived from its root's name.
|
|
40
49
|
#
|
|
41
50
|
def save_one( name )
|
|
42
|
-
|
|
43
|
-
|
|
51
|
+
obj = resolve_for_save( name )
|
|
52
|
+
return unless obj
|
|
53
|
+
|
|
54
|
+
fs_list = find_file_storages( obj )
|
|
55
|
+
return save_new( obj, @mech.resolve_save_path( root_of( obj ).name ) ) if fs_list.empty?
|
|
44
56
|
|
|
45
|
-
# Send an on_save event
|
|
46
57
|
@engine.event_manager.on_save obj
|
|
58
|
+
save_batch( fs_list )
|
|
59
|
+
end
|
|
47
60
|
|
|
48
|
-
|
|
49
|
-
|
|
61
|
+
#
|
|
62
|
+
# Save the given object to an explicit path. Registers the
|
|
63
|
+
# mapping so a future bare save includes it. Refuses to overwrite
|
|
64
|
+
# a file that exists but isn't already mapped to this object.
|
|
65
|
+
#
|
|
66
|
+
def save_to( name, path )
|
|
67
|
+
obj = resolve_for_save( name )
|
|
68
|
+
return unless obj
|
|
69
|
+
|
|
70
|
+
pn = @mech.resolve_save_path( path )
|
|
71
|
+
mapped = @maps.find { |fs| fs.pn == pn }
|
|
72
|
+
return save_mapped( obj, mapped, pn ) if mapped
|
|
73
|
+
return @engine.err( "#{PATH_EXISTS_ERR}#{pn}" ) if @mech.exist?( pn )
|
|
74
|
+
|
|
75
|
+
save_new( obj, pn )
|
|
50
76
|
end
|
|
51
77
|
|
|
52
78
|
#
|
|
53
|
-
# Load the object from the file.
|
|
79
|
+
# Load the object(s) from the file (or files, for a wildcard).
|
|
80
|
+
# Returns true if at least one object was loaded, false if the
|
|
81
|
+
# name couldn't be resolved to any file or nothing loaded --
|
|
82
|
+
# a missing/unresolvable file is reported via engine.err rather
|
|
83
|
+
# than failing silently.
|
|
54
84
|
#
|
|
55
85
|
def load( name )
|
|
56
86
|
pns = get_full_path_names name
|
|
57
|
-
|
|
87
|
+
if pns.nil? || pns.empty?
|
|
88
|
+
@engine.err( "#{FILE_NOT_FOUND_ERR}#{name}" ) unless name.to_s.strip.empty?
|
|
89
|
+
return false
|
|
90
|
+
end
|
|
58
91
|
|
|
92
|
+
loaded = false
|
|
59
93
|
pns.each do |pn|
|
|
60
94
|
@engine.log.debug "Load file(s) at: #{pn}"
|
|
61
95
|
begin
|
|
62
96
|
fs = Gloo::Persist::FileStorage.new( @engine, pn )
|
|
63
97
|
fs.load
|
|
98
|
+
next unless fs.obj # a failed load -- don't map it
|
|
99
|
+
|
|
64
100
|
@maps << fs
|
|
65
101
|
@engine.event_manager.on_load fs.obj
|
|
102
|
+
loaded = true
|
|
66
103
|
rescue => ex
|
|
67
104
|
@engine.handle_exception( ex )
|
|
68
105
|
end
|
|
69
106
|
end
|
|
107
|
+
return loaded
|
|
70
108
|
end
|
|
71
109
|
|
|
72
110
|
#
|
|
@@ -74,8 +112,8 @@ module Gloo
|
|
|
74
112
|
# The engine is reset to a clean state.
|
|
75
113
|
#
|
|
76
114
|
def unload_all
|
|
77
|
-
objs = self.maps.map
|
|
78
|
-
objs.each
|
|
115
|
+
objs = self.maps.map( &:obj ).compact
|
|
116
|
+
objs.each( &:msg_unload )
|
|
79
117
|
@engine.reset_state
|
|
80
118
|
end
|
|
81
119
|
|
|
@@ -88,22 +126,21 @@ module Gloo
|
|
|
88
126
|
def unload( obj )
|
|
89
127
|
@engine.event_manager.on_unload obj
|
|
90
128
|
@engine.heap.unload obj
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
end
|
|
96
|
-
end
|
|
129
|
+
# Drop every mapping for this object -- more than one when the
|
|
130
|
+
# root came from several files (the namespace pattern) -- and
|
|
131
|
+
# sweep out any mapping whose object is already gone.
|
|
132
|
+
@maps.reject! { |o| o.obj.nil? || ( o.obj.pn === obj.pn ) }
|
|
97
133
|
end
|
|
98
134
|
|
|
99
|
-
#
|
|
135
|
+
#
|
|
100
136
|
# Reload all objects.
|
|
101
137
|
# First send a message to each object to let it know it is being reloaded.
|
|
102
138
|
# Then let the engine restart to reload the files.
|
|
103
|
-
#
|
|
139
|
+
#
|
|
104
140
|
def reload_all
|
|
105
141
|
return unless @maps
|
|
106
|
-
|
|
142
|
+
|
|
143
|
+
@maps.each { |fs| warn_if_dirty( fs ) }
|
|
107
144
|
@maps.each do |fs|
|
|
108
145
|
@engine.event_manager.on_reload fs.obj
|
|
109
146
|
end
|
|
@@ -112,23 +149,24 @@ module Gloo
|
|
|
112
149
|
@engine.restart
|
|
113
150
|
end
|
|
114
151
|
|
|
115
|
-
#
|
|
152
|
+
#
|
|
116
153
|
# Re-load the given object from file.
|
|
117
154
|
# This is used to reload a single object and comes from a
|
|
118
155
|
# message sent to the object.
|
|
119
|
-
#
|
|
156
|
+
#
|
|
120
157
|
def reload( obj )
|
|
121
158
|
fs = find_file_storage( obj )
|
|
122
159
|
return unless fs
|
|
123
160
|
|
|
161
|
+
warn_if_dirty( fs )
|
|
124
162
|
@engine.event_manager.on_reload obj
|
|
125
163
|
@engine.heap.unload obj
|
|
126
164
|
fs.load
|
|
127
165
|
end
|
|
128
166
|
|
|
129
|
-
#
|
|
167
|
+
#
|
|
130
168
|
# Find the objects FileStorage in the list.
|
|
131
|
-
#
|
|
169
|
+
#
|
|
132
170
|
def find_file_storage( obj )
|
|
133
171
|
@maps.each do |o|
|
|
134
172
|
return o if ( o.obj.pn === obj.pn )
|
|
@@ -138,6 +176,16 @@ module Gloo
|
|
|
138
176
|
return nil
|
|
139
177
|
end
|
|
140
178
|
|
|
179
|
+
#
|
|
180
|
+
# Find every FileStorage that owns a declaration in the given
|
|
181
|
+
# object's root -- usually one, more when the root is shared
|
|
182
|
+
# across files (the multi-file namespace pattern).
|
|
183
|
+
#
|
|
184
|
+
def find_file_storages( obj )
|
|
185
|
+
root = root_of( obj )
|
|
186
|
+
return @maps.select { |fs| fs.roots.include?( root ) }
|
|
187
|
+
end
|
|
188
|
+
|
|
141
189
|
|
|
142
190
|
#
|
|
143
191
|
# Get the full path and name of the file.
|
|
@@ -176,6 +224,125 @@ module Gloo
|
|
|
176
224
|
end
|
|
177
225
|
end
|
|
178
226
|
|
|
227
|
+
private
|
|
228
|
+
|
|
229
|
+
#
|
|
230
|
+
# Resolve a name to an object for saving, reporting an error and
|
|
231
|
+
# returning nil if it can't be found.
|
|
232
|
+
#
|
|
233
|
+
def resolve_for_save( name )
|
|
234
|
+
obj = Gloo::Core::Pn.new( @engine, name ).resolve
|
|
235
|
+
@engine.err( "#{OBJ_NOT_FOUND_ERR}#{name}" ) unless obj
|
|
236
|
+
return obj
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
#
|
|
240
|
+
# Walk up to the top-level declared object that contains obj --
|
|
241
|
+
# its parent is the heap root, not another declared object.
|
|
242
|
+
#
|
|
243
|
+
def root_of( obj )
|
|
244
|
+
o = obj
|
|
245
|
+
o = o.parent until o.parent.nil? || o.parent.root?
|
|
246
|
+
return o
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
#
|
|
250
|
+
# Save a set of files as one batch so a multi-file namespace
|
|
251
|
+
# round-trips: each file only rewrites the declarations its own
|
|
252
|
+
# SourceDoc holds and leaves other files' declarations alone; a
|
|
253
|
+
# brand-new object no file owns is written once, by the first file
|
|
254
|
+
# in the batch to reach it.
|
|
255
|
+
#
|
|
256
|
+
def save_batch( files )
|
|
257
|
+
claimed = {}.compare_by_identity
|
|
258
|
+
files.each do |fs|
|
|
259
|
+
fs.save( :others => owned_elsewhere( fs ), :claimed => claimed )
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
#
|
|
264
|
+
# Every heap object declared by some loaded file's SourceDoc
|
|
265
|
+
# other than fs.
|
|
266
|
+
#
|
|
267
|
+
def owned_elsewhere( fs )
|
|
268
|
+
owned = {}.compare_by_identity
|
|
269
|
+
@maps.each do |other|
|
|
270
|
+
next if other.equal?( fs )
|
|
271
|
+
|
|
272
|
+
collect_owned( other.source_doc&.children, owned )
|
|
273
|
+
end
|
|
274
|
+
return owned
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
#
|
|
278
|
+
# Recursively collect the objects declared by the given source
|
|
279
|
+
# nodes into owned.
|
|
280
|
+
#
|
|
281
|
+
def collect_owned( nodes, owned )
|
|
282
|
+
return unless nodes
|
|
283
|
+
|
|
284
|
+
nodes.each do |node|
|
|
285
|
+
next unless node.is_a?( Gloo::Persist::Source::ObjNode )
|
|
286
|
+
|
|
287
|
+
if node.obj
|
|
288
|
+
owned[ node.obj ] = true
|
|
289
|
+
mark_shorthand_ancestors( node, owned )
|
|
290
|
+
end
|
|
291
|
+
collect_owned( node.children, owned )
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
#
|
|
296
|
+
# A nested-container shorthand declaration (a.b.c [type] :) only
|
|
297
|
+
# records a source node for the leaf; the intermediate containers
|
|
298
|
+
# (a, a.b) are real heap objects with no node of their own. Mark
|
|
299
|
+
# them owned too, walking up from the leaf, so another file's save
|
|
300
|
+
# doesn't see an intermediate as an orphan and adopt its whole
|
|
301
|
+
# subtree.
|
|
302
|
+
#
|
|
303
|
+
def mark_shorthand_ancestors( node, owned )
|
|
304
|
+
return unless node.name.include?( '.' )
|
|
305
|
+
|
|
306
|
+
o = node.obj.parent
|
|
307
|
+
while o && !o.root?
|
|
308
|
+
owned[ o ] = true
|
|
309
|
+
o = o.parent
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
#
|
|
314
|
+
# Save to a path that's already mapped to some file. If that
|
|
315
|
+
# mapping is for a different root, it's a real collision -- the
|
|
316
|
+
# path belongs to something else.
|
|
317
|
+
#
|
|
318
|
+
def save_mapped( obj, mapped, pn )
|
|
319
|
+
return @engine.err( "#{PATH_EXISTS_ERR}#{pn}" ) unless mapped.roots.include?( root_of( obj ) )
|
|
320
|
+
|
|
321
|
+
@engine.event_manager.on_save obj
|
|
322
|
+
save_batch( [ mapped ] )
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
#
|
|
326
|
+
# Save an object's root fresh to a new path, and register the
|
|
327
|
+
# resulting mapping.
|
|
328
|
+
#
|
|
329
|
+
def save_new( obj, pn )
|
|
330
|
+
@engine.event_manager.on_save obj
|
|
331
|
+
fs = Gloo::Persist::FileStorage.new( @engine, pn, root_of( obj ) )
|
|
332
|
+
fs.save
|
|
333
|
+
@maps << fs
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
#
|
|
337
|
+
# Warn (without blocking) if reloading fs would discard changes
|
|
338
|
+
# that haven't been saved -- any object whose value no longer
|
|
339
|
+
# round-trips to what's on disk.
|
|
340
|
+
#
|
|
341
|
+
def warn_if_dirty( fs )
|
|
342
|
+
saver = Gloo::Persist::FileSaver.new( @engine, fs.pn, fs.obj, fs.source_doc )
|
|
343
|
+
@engine.log.warn "#{RELOAD_DIRTY_WARNING}#{fs.pn}" if saver.dirty?
|
|
344
|
+
end
|
|
345
|
+
|
|
179
346
|
end
|
|
180
347
|
end
|
|
181
348
|
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Author:: Eric Crane (mailto:eric.crane@mac.com)
|
|
2
|
+
# Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# Collects an indented, unparsed script body (no BEGIN/END) for the
|
|
5
|
+
# object that was just declared. We don't yet know whether a body
|
|
6
|
+
# actually follows -- that's decided by the next line's indentation,
|
|
7
|
+
# so this may close immediately with nothing collected (an empty
|
|
8
|
+
# script).
|
|
9
|
+
#
|
|
10
|
+
|
|
11
|
+
module Gloo
|
|
12
|
+
module Persist
|
|
13
|
+
class ScriptBodyCollector
|
|
14
|
+
|
|
15
|
+
attr_reader :active
|
|
16
|
+
|
|
17
|
+
#
|
|
18
|
+
# Set up an inactive collector.
|
|
19
|
+
#
|
|
20
|
+
def initialize
|
|
21
|
+
@active = false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
#
|
|
25
|
+
# Start tentatively collecting a body for the object just
|
|
26
|
+
# declared. node is its Source::ObjNode, heap_obj the object
|
|
27
|
+
# itself, base_tabs the indentation of the declaration line
|
|
28
|
+
# (body lines must be indented deeper than this).
|
|
29
|
+
#
|
|
30
|
+
def start( node, heap_obj, base_tabs )
|
|
31
|
+
@active = true
|
|
32
|
+
@node = node
|
|
33
|
+
@obj = heap_obj
|
|
34
|
+
@base_tabs = base_tabs
|
|
35
|
+
@lines = []
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
#
|
|
39
|
+
# One line while a body might be open. line_tabs is its
|
|
40
|
+
# indentation (ignored for a blank line, which is always kept).
|
|
41
|
+
# Returns true if the line was consumed as body content, or
|
|
42
|
+
# false if it closed the body and still needs to be dispatched
|
|
43
|
+
# normally by the caller.
|
|
44
|
+
#
|
|
45
|
+
def handle_line( line, line_tabs )
|
|
46
|
+
if line.strip.empty?
|
|
47
|
+
@lines << chomped( line )
|
|
48
|
+
return true
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
if line_tabs <= @base_tabs
|
|
52
|
+
close
|
|
53
|
+
return false
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
add_line( line )
|
|
57
|
+
return true
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
#
|
|
61
|
+
# End of file with a body still open (eg. the last object in the
|
|
62
|
+
# file is an empty or in-progress script) -- close it so its
|
|
63
|
+
# content isn't silently dropped.
|
|
64
|
+
#
|
|
65
|
+
def finish
|
|
66
|
+
close if @active
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
#
|
|
72
|
+
# Record one real body line: kept verbatim for the source model;
|
|
73
|
+
# added as a runnable command only if it isn't a comment (a
|
|
74
|
+
# comment inside a script body is documentation, not something
|
|
75
|
+
# to execute).
|
|
76
|
+
#
|
|
77
|
+
def add_line( line )
|
|
78
|
+
@lines << chomped( line )
|
|
79
|
+
@obj.add_line( line ) unless line.strip.start_with?( '#' )
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
#
|
|
83
|
+
# Close out the body, recording its raw content on the node.
|
|
84
|
+
#
|
|
85
|
+
def close
|
|
86
|
+
@active = false
|
|
87
|
+
@node.raw_value = @lines.join( "\n" )
|
|
88
|
+
@node.block_style = :body unless @lines.empty?
|
|
89
|
+
@node = nil
|
|
90
|
+
@obj = nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
#
|
|
94
|
+
# A line's raw text with its trailing newline removed.
|
|
95
|
+
#
|
|
96
|
+
def chomped( line )
|
|
97
|
+
return line.chomp( "\n" )
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Author:: Eric Crane (mailto:eric.crane@mac.com)
|
|
2
|
+
# Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# Nested container shorthand: a dotted name in a declaration
|
|
5
|
+
# (page.core.users.list [type] :) means "find or create a container
|
|
6
|
+
# for each prefix segment, then declare the real object under the
|
|
7
|
+
# last one". A segment that already names some other kind of object
|
|
8
|
+
# is used as-is, not forced to a container.
|
|
9
|
+
#
|
|
10
|
+
|
|
11
|
+
module Gloo
|
|
12
|
+
module Persist
|
|
13
|
+
class ShorthandExpander
|
|
14
|
+
|
|
15
|
+
#
|
|
16
|
+
# Set up an expander for the given engine.
|
|
17
|
+
#
|
|
18
|
+
def initialize( engine )
|
|
19
|
+
@engine = engine
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
#
|
|
23
|
+
# Split a possibly-dotted name against the given parent. Returns
|
|
24
|
+
# [leaf_name, parent_for_the_leaf, roots_touched, created]:
|
|
25
|
+
# - roots_touched: containers created OR reused directly under
|
|
26
|
+
# the heap root, for the loader to register as this file's roots
|
|
27
|
+
# - created: containers this call actually created (any level),
|
|
28
|
+
# for the loader to remember as its own
|
|
29
|
+
#
|
|
30
|
+
def expand( name, parent )
|
|
31
|
+
return [ name, parent, [], [] ] unless name.include?( '.' )
|
|
32
|
+
|
|
33
|
+
segments = name.split( '.' )
|
|
34
|
+
leaf = segments.pop
|
|
35
|
+
roots = []
|
|
36
|
+
created = []
|
|
37
|
+
segments.each do |seg|
|
|
38
|
+
child = parent.find_child( seg )
|
|
39
|
+
unless child
|
|
40
|
+
child = @engine.factory.create_can( seg, parent )
|
|
41
|
+
created << child
|
|
42
|
+
end
|
|
43
|
+
roots << child if parent == @engine.heap.root
|
|
44
|
+
parent = child
|
|
45
|
+
end
|
|
46
|
+
return [ leaf, parent, roots, created ]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Author:: Eric Crane (mailto:eric.crane@mac.com)
|
|
2
|
+
# Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# A blank line between declarations. Kept (not discarded) so a save
|
|
5
|
+
# can reproduce the file's original spacing.
|
|
6
|
+
#
|
|
7
|
+
|
|
8
|
+
module Gloo
|
|
9
|
+
module Persist
|
|
10
|
+
module Source
|
|
11
|
+
class BlankNode
|
|
12
|
+
|
|
13
|
+
attr_reader :raw
|
|
14
|
+
|
|
15
|
+
#
|
|
16
|
+
# Set up a blank line node. raw is the full source line, minus
|
|
17
|
+
# its trailing newline (may be non-empty if the blank line had
|
|
18
|
+
# trailing whitespace -- preserved rather than normalized).
|
|
19
|
+
#
|
|
20
|
+
def initialize( raw = '' )
|
|
21
|
+
@raw = raw
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Author:: Eric Crane (mailto:eric.crane@mac.com)
|
|
2
|
+
# Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# A whole-line comment that is not attached to any object declaration
|
|
5
|
+
# as its leading_doc -- either because a blank line separated it from
|
|
6
|
+
# the declaration that follows, or nothing follows it at all.
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
module Gloo
|
|
10
|
+
module Persist
|
|
11
|
+
module Source
|
|
12
|
+
class CommentNode
|
|
13
|
+
|
|
14
|
+
attr_reader :raw
|
|
15
|
+
|
|
16
|
+
#
|
|
17
|
+
# Set up a comment node. raw is the full source line, minus
|
|
18
|
+
# its trailing newline.
|
|
19
|
+
#
|
|
20
|
+
def initialize( raw )
|
|
21
|
+
@raw = raw
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Author:: Eric Crane (mailto:eric.crane@mac.com)
|
|
2
|
+
# Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# A top-of-file 'load lib {name}' (or 'load ext {name}') directive,
|
|
5
|
+
# run by the loader itself before the declarations that follow it.
|
|
6
|
+
#
|
|
7
|
+
|
|
8
|
+
module Gloo
|
|
9
|
+
module Persist
|
|
10
|
+
module Source
|
|
11
|
+
class DirectiveNode
|
|
12
|
+
|
|
13
|
+
attr_reader :raw
|
|
14
|
+
|
|
15
|
+
#
|
|
16
|
+
# Set up a directive node. raw is the full source line, minus
|
|
17
|
+
# its trailing newline.
|
|
18
|
+
#
|
|
19
|
+
def initialize( raw )
|
|
20
|
+
@raw = raw
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Author:: Eric Crane (mailto:eric.crane@mac.com)
|
|
2
|
+
# Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# One object declaration in a source file, eg.
|
|
5
|
+
# name [type] : value
|
|
6
|
+
# Carries the raw text needed to reproduce the declaration unchanged,
|
|
7
|
+
# plus a back-reference to the heap object it created (or updated),
|
|
8
|
+
# so a save can tell whether the live value still matches what's on
|
|
9
|
+
# disk and, if so, leave the raw text alone.
|
|
10
|
+
#
|
|
11
|
+
|
|
12
|
+
module Gloo
|
|
13
|
+
module Persist
|
|
14
|
+
module Source
|
|
15
|
+
class ObjNode
|
|
16
|
+
|
|
17
|
+
# block_style is one of:
|
|
18
|
+
# :inline -- name [type] : value, all on one line
|
|
19
|
+
# :begin_end -- name [type] : BEGIN ... END
|
|
20
|
+
# :body -- name [type] :, followed by an indented,
|
|
21
|
+
# unparsed body (script commands)
|
|
22
|
+
attr_accessor :name, :raw_type, :raw_indent, :raw_tail, :block_style,
|
|
23
|
+
:raw_value, :raw_end_indent, :leading_doc, :trailing_comment, :obj
|
|
24
|
+
attr_reader :children
|
|
25
|
+
|
|
26
|
+
#
|
|
27
|
+
# Set up an object declaration node. Everything but the name
|
|
28
|
+
# and raw type is optional and commonly set afterward via the
|
|
29
|
+
# accessors above -- keeping the initializer small.
|
|
30
|
+
#
|
|
31
|
+
def initialize( name:, raw_type: )
|
|
32
|
+
@name = name
|
|
33
|
+
@raw_type = raw_type
|
|
34
|
+
@raw_indent = ''
|
|
35
|
+
@raw_tail = ''
|
|
36
|
+
@block_style = :inline
|
|
37
|
+
@raw_value = nil
|
|
38
|
+
# a begin_end block's closing END is not necessarily indented
|
|
39
|
+
# to match its declaration -- kept separately, raw.
|
|
40
|
+
@raw_end_indent = ''
|
|
41
|
+
@leading_doc = nil
|
|
42
|
+
@trailing_comment = nil
|
|
43
|
+
@obj = nil
|
|
44
|
+
@children = []
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
#
|
|
48
|
+
# The cleaned-up leading_doc: each line's leading whitespace and
|
|
49
|
+
# its '#' marker (plus one following space, if any) stripped,
|
|
50
|
+
# then the result dedented to its shallowest line. The full
|
|
51
|
+
# block is kept as written, blank '#' lines at the top/bottom
|
|
52
|
+
# included -- it's a faithful reproduction of the comment, not
|
|
53
|
+
# a trimmed summary. '' when there's no leading_doc at all.
|
|
54
|
+
#
|
|
55
|
+
def doc
|
|
56
|
+
return '' unless @leading_doc
|
|
57
|
+
|
|
58
|
+
lines = @leading_doc.split( "\n" ).map { |l| strip_marker( l ) }
|
|
59
|
+
return dedent( lines ).join( "\n" )
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
#
|
|
65
|
+
# One raw comment line -> its text past the '#': leading
|
|
66
|
+
# whitespace dropped, then the '#' and at most one space after
|
|
67
|
+
# it, then trailing whitespace dropped.
|
|
68
|
+
#
|
|
69
|
+
def strip_marker( raw )
|
|
70
|
+
return raw.lstrip.sub( /\A#\x20?/, '' ).rstrip
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
#
|
|
74
|
+
# Remove the common leading whitespace shared by every non-blank
|
|
75
|
+
# line, so a comment block indented for readability (eg. a
|
|
76
|
+
# bullet list within it) keeps its relative indentation.
|
|
77
|
+
#
|
|
78
|
+
def dedent( lines )
|
|
79
|
+
non_blank = lines.reject( &:empty? )
|
|
80
|
+
return lines if non_blank.empty?
|
|
81
|
+
|
|
82
|
+
n = non_blank.map { |l| l[ /\A */ ].length }.min
|
|
83
|
+
return lines if n.zero?
|
|
84
|
+
|
|
85
|
+
return lines.map { |l| l.empty? ? l : l[ n.. ] }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Author:: Eric Crane (mailto:eric.crane@mac.com)
|
|
2
|
+
# Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# The concrete source model for one loaded file: an ordered list of
|
|
5
|
+
# nodes (comments, blank lines, directives, and object declarations)
|
|
6
|
+
# that mirrors what the loader actually read, so a save can rewrite
|
|
7
|
+
# the file instead of regenerating it from the heap.
|
|
8
|
+
#
|
|
9
|
+
# The top level plays the same role as an Source::ObjNode's children
|
|
10
|
+
# list -- there is no special "root" wrapper, just the file's own
|
|
11
|
+
# ordered content, which may include more than one top-level object
|
|
12
|
+
# declaration (a file may legitimately declare several roots).
|
|
13
|
+
#
|
|
14
|
+
|
|
15
|
+
module Gloo
|
|
16
|
+
module Persist
|
|
17
|
+
module Source
|
|
18
|
+
class SourceDoc
|
|
19
|
+
|
|
20
|
+
attr_reader :children
|
|
21
|
+
|
|
22
|
+
#
|
|
23
|
+
# Set up an empty source document.
|
|
24
|
+
#
|
|
25
|
+
def initialize
|
|
26
|
+
@children = []
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
#
|
|
30
|
+
# The top-level object declarations in this file (usually one,
|
|
31
|
+
# per the "a file owns its declarations" model -- more than one
|
|
32
|
+
# only when the file legitimately declares several roots).
|
|
33
|
+
#
|
|
34
|
+
def roots
|
|
35
|
+
return @children.select { |n| n.is_a?( Source::ObjNode ) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|