gloo 6.1.0 → 6.2.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.
data/docs/web_app.md ADDED
@@ -0,0 +1,211 @@
1
+ # A Trivial Web App
2
+
3
+ **Contents**
4
+
5
+ - Overview
6
+ - File and Folder Layout
7
+ - start.gloo
8
+ - app/app.gloo
9
+ - layout/primary.gloo
10
+ - shared/nav.gloo
11
+ - page/home.gloo
12
+ - page/about.gloo
13
+ - Running It
14
+ - Where To Go From Here
15
+
16
+ ## Overview
17
+
18
+ This walks through the smallest complete gloo web app: a "Hello World" with two pages (`home` and `about`) sharing a common layout and nav bar. It uses the `gloo-web` core library — see Plugins for how core libraries are installed and loaded in general.
19
+
20
+ The app runs in App Mode (`gloo --app {path}`, see Application) — gloo looks for a `start.gloo` file at the root of the app folder and runs it. That file loads everything else and starts the web server.
21
+
22
+ ## File and Folder Layout
23
+
24
+ ```
25
+ hello/
26
+ start.gloo
27
+ app/
28
+ app.gloo
29
+ layout/
30
+ primary.gloo
31
+ shared/
32
+ nav.gloo
33
+ page/
34
+ home.gloo
35
+ about.gloo
36
+ ```
37
+
38
+ - `start.gloo` — the entry point. Loads the `web` core library, then everything under `app/`, `layout/`, `shared/`, and `page/`, then starts the server.
39
+ - `app/app.gloo` — the `app` object: the app's URL and its `svr` (web server), including server config and the default routes (layout, home page).
40
+ - `layout/primary.gloo` — the page layout shared by every page: the HTML shell, the nav bar, and where a page's own head/body content gets inserted.
41
+ - `shared/nav.gloo` — a partial (reusable fragment) for the nav bar, included by the layout.
42
+ - `page/home.gloo`, `page/about.gloo` — the two pages. Every `[page]` object loaded under the root `page` container automatically becomes a route: `page.home` answers `/`, `page.about` answers `/about`. Routes are wired up by the router when the server starts — there's no separate routing table to maintain by hand.
43
+
44
+ ## start.gloo
45
+
46
+ ```gloo
47
+ #
48
+ # Start the hello app.
49
+ #
50
+ # Run it:
51
+ # gloo --app ~/gloo/projects/apps/hello
52
+ #
53
+
54
+ start [container] :
55
+
56
+ on_load [script] :
57
+
58
+ # Load the web core library.
59
+ load lib web
60
+
61
+ # Load the app's own objects.
62
+ load app/*
63
+ load layout/*
64
+ load shared/*
65
+ load page/*
66
+
67
+ # Start the server and open it in a browser.
68
+ tell app.svr to start
69
+ tell app.url to open
70
+ ```
71
+
72
+ ## app/app.gloo
73
+
74
+ ```gloo
75
+ #
76
+ # The hello app: web server config and default routes.
77
+ #
78
+
79
+ app [can] :
80
+
81
+ # The app's URL.
82
+ url [uri] : http://localhost:8080/
83
+
84
+ # The web server for the app.
85
+ svr [svr] :
86
+
87
+ config [container] :
88
+ scheme [string] : http
89
+ host [string] : localhost
90
+ port [string] : 8080
91
+
92
+ on_start [script] : show 'hello app started'
93
+ on_stop [script] : show 'hello app stopped'
94
+
95
+ # Default layout and home page.
96
+ layout [alias] : layout.primary
97
+ home [alias] : page.home
98
+ ```
99
+
100
+ `svr` is a `gloo-web` object type (see Plugins — Core Libraries). `layout` and `home` are conventional aliases: `layout` points at the partial used to wrap every page that doesn't specify its own, and `home` points at the page served for `/`. A real app would also set `error [alias] : page.err` to control the page shown on a server error, but it's optional — gloo falls back to a generic message if it's not set.
101
+
102
+ ## layout/primary.gloo
103
+
104
+ ```gloo
105
+ #
106
+ # The shared page layout: HTML shell + nav bar.
107
+ # Every page renders inside this unless it specifies its own layout.
108
+ #
109
+
110
+ layout [can] :
111
+ primary [part] :
112
+ content [can] :
113
+
114
+ html_open [text] : BEGIN
115
+ <!DOCTYPE html>
116
+ <html lang="en">
117
+ <head>
118
+ <%= head %>
119
+ </head>
120
+ <body>
121
+ END
122
+
123
+ nav [alias] : shared.nav
124
+ body [string] : <%= body %>
125
+
126
+ html_close [text] : BEGIN
127
+ </body>
128
+ </html>
129
+ END
130
+ ```
131
+
132
+ `primary` is a `[part]` (partial) — see Plugins for the `gloo-web` object list. `<%= head %>` and `<%= body %>` are filled in automatically by the page being rendered: whatever that page defines under its own `head` and `body` children. `nav` is an alias to the shared nav partial below, rendered inline wherever it appears in the layout's content.
133
+
134
+ ## shared/nav.gloo
135
+
136
+ ```gloo
137
+ #
138
+ # The shared nav bar, included by the layout.
139
+ #
140
+
141
+ shared [can] :
142
+ nav [part] :
143
+ content [text] : BEGIN
144
+ <nav>
145
+ <a href="/">Home</a>
146
+ <a href="/about">About</a>
147
+ </nav>
148
+ END
149
+ ```
150
+
151
+ ## page/home.gloo
152
+
153
+ ```gloo
154
+ #
155
+ # The home page ("/").
156
+ #
157
+
158
+ page [can] :
159
+ home [page] :
160
+
161
+ params [can] :
162
+ msg [string] : Hello, World!
163
+
164
+ head [e] :
165
+ content [can] :
166
+ title [e] : Hello
167
+
168
+ body [e] :
169
+ content [can] :
170
+ h1 [e] : <%= msg %>
171
+ p [e] : This is the home page.
172
+ ```
173
+
174
+ `page.home` is what `app.svr.home` points to, so it answers requests to `/`. `params` declares values the page's content can reference — here just `msg`, used in the `<h1>` via `<%= msg %>`. `head` and `title` and `body`, `h1`, `p` are `[e]` (element) objects — see Plugins — the building blocks the layout's `<%= head %>`/`<%= body %>` render.
175
+
176
+ ## page/about.gloo
177
+
178
+ ```gloo
179
+ #
180
+ # The about page ("/about"), reached by name automatically —
181
+ # no routing table entry required.
182
+ #
183
+
184
+ page [can] :
185
+ about [page] :
186
+
187
+ head [e] :
188
+ content [can] :
189
+ title [e] : About
190
+
191
+ body [e] :
192
+ content [can] :
193
+ h1 [e] : About
194
+ p [e] : A trivial two-page gloo web app.
195
+ ```
196
+
197
+ Because this page is named `about` and lives directly under the root `page` container, it's automatically routed to `/about` the moment the server starts — see File and Folder Layout above.
198
+
199
+ ## Running It
200
+
201
+ ```shell
202
+ gloo --app ~/gloo/projects/apps/hello
203
+ ```
204
+
205
+ This runs `start.gloo`, which starts the server and opens `http://localhost:8080/` in a browser. Visit `/about` to see the second page. Stop the app the same way as any other gloo app (`q` at the prompt, or `Ctrl-C`).
206
+
207
+ Once the app is running, `gloo-web`'s objects (`page`, `part`, `e`, `svr`, `form`, `field`) are all documented in-app — `help> object page`, etc. — see Plugins.
208
+
209
+ ## Where To Go From Here
210
+
211
+ This example deliberately leaves out most of what `gloo-web` supports: forms and fields (`form`, `field`), page parameters populated from query strings, sessions, static assets (images/CSS/JS served from an `asset/` folder, with `image_tag`/`css_tag`/`js_tag` layout helpers), a database-backed page (`gloo-db` plus a `sqlite`/`mysql`/`postgres` driver — see Plugins), and a dedicated error page. Each of those follows the same shape shown here: an object type from `gloo-web` (or another core library), loaded the same way, documented the same way in `help`.
data/lib/VERSION CHANGED
@@ -1 +1 @@
1
- 6.1.0
1
+ 6.2.0
data/lib/VERSION_NOTES CHANGED
@@ -1,3 +1,9 @@
1
+ 6.2.0 - 2026.08.18
2
+ - Adds light | dark mode option for terminal output.
3
+ - Adds inline function invocation.
4
+ - Adds split messages for strings and text objects.
5
+
6
+
1
7
  6.1.0 - 2026.08.04
2
8
  - Adds narrative documentation: 10 pages in `docs/`, covering getting started, application, language objects/syntax/scripting, operators, iterators, objects, verbs, and plugins
3
9
  - Adds an in-app interactive help shell (`help`/`?`) to look up verbs, objects, settings, libraries, extensions, and doc pages, with tab completion
@@ -31,10 +31,14 @@ module Gloo
31
31
  @args = Args.new( self, context.params )
32
32
  @settings = Settings.new( self, context.user_root )
33
33
 
34
+ # Platform (and its theme) needs to be ready before Log is
35
+ # constructed - Log reads engine.theme, which reads through
36
+ # to platform.theme, in its own initialize.
37
+ @platform = context.platform
38
+ @platform.theme = Gloo::App::Theme.new( @settings.theme )
39
+
34
40
  @log = context.log.new( self, @args.quiet? )
35
41
  @log.debug "log (class: #{@log.class.name}) in use ..."
36
-
37
- @platform = context.platform
38
42
  @log.debug "platform (class: #{@platform.class.name}) in use ..."
39
43
 
40
44
  @handling_exception = false
@@ -42,6 +46,17 @@ module Gloo
42
46
  @log.debug 'engine intialized...'
43
47
  end
44
48
 
49
+ #
50
+ # Get the active theme. Platform is the single source of
51
+ # truth for it (Prompt/Table only hold a @platform reference,
52
+ # not an @engine one) - this just reads through to that,
53
+ # rather than keeping a second ivar that could drift out of
54
+ # sync if something reassigns platform.theme directly.
55
+ #
56
+ def theme
57
+ return @platform.theme
58
+ end
59
+
45
60
  #
46
61
  # Start the engine.
47
62
  # Load object and verb definitions and setup engine elements.
data/lib/gloo/app/log.rb CHANGED
@@ -38,6 +38,7 @@ module Gloo
38
38
  @engine = engine
39
39
  @quiet = quiet
40
40
  @debug = engine.settings.debug
41
+ @theme = engine.theme
41
42
 
42
43
  create_loggers
43
44
 
@@ -160,7 +161,7 @@ module Gloo
160
161
  def warn( msg )
161
162
  @logger.warn msg
162
163
  @error.warn msg
163
- puts msg.yellow unless @quiet
164
+ puts @theme.warn( msg ) unless @quiet
164
165
  end
165
166
 
166
167
  #
@@ -175,11 +176,11 @@ module Gloo
175
176
  if ex
176
177
  @error.error ex.message
177
178
  @error.error ex.backtrace
178
- puts msg.red unless @quiet
179
- puts ex.message.red unless @quiet
179
+ puts @theme.error( msg ) unless @quiet
180
+ puts @theme.error( ex.message ) unless @quiet
180
181
  puts ex.backtrace unless @quiet
181
182
  else
182
- puts msg.red unless @quiet
183
+ puts @theme.error( msg ) unless @quiet
183
184
  end
184
185
  end
185
186
 
@@ -18,6 +18,7 @@ module Gloo
18
18
  PAGER_CMD = 'less -R -F -X'.freeze
19
19
 
20
20
  attr_reader :prompt, :table
21
+ attr_accessor :theme
21
22
 
22
23
  #
23
24
  # Set up Platform.
@@ -25,6 +26,10 @@ module Gloo
25
26
  def initialize
26
27
  @prompt = Gloo::App::Prompt.new( self )
27
28
  @table = Gloo::App::Table.new( self )
29
+
30
+ # Default theme, in case this platform is ever used
31
+ # standalone, without an Engine to set the real one.
32
+ @theme = Gloo::App::Theme.new
28
33
  end
29
34
 
30
35
  #
@@ -87,7 +87,8 @@ module Gloo
87
87
  dt = DateTime.now
88
88
  d = dt.strftime( '%Y.%m.%d' )
89
89
  t = dt.strftime( '%I:%M:%S' )
90
- return "#{'gloo'.blue} #{d.yellow} #{t.white} >"
90
+ theme = @platform.theme
91
+ return "#{theme.heading( 'gloo' )} #{theme.accent( d )} #{theme.emphasis( t )} >"
91
92
  end
92
93
 
93
94
  end
@@ -10,10 +10,13 @@ module Gloo
10
10
  module App
11
11
  class Settings
12
12
 
13
+ DEFAULT_THEME = 'dark'.freeze
14
+ THEMES = %w[dark light].freeze
15
+
13
16
  attr_reader :user_root, :log_path,
14
17
  :config_path, :project_path, :ext_path,
15
18
  :start_with, :list_indent, :list_levels, :tmp_path,
16
- :debug_path, :debug
19
+ :debug_path, :debug, :theme
17
20
 
18
21
  #
19
22
  # Load setting from the yml file.
@@ -33,26 +36,29 @@ module Gloo
33
36
  # Can be seen in app with 'help settings'
34
37
  #
35
38
  def show
36
- puts "\n Application Settings:".blue
37
- puts ' Startup with: '.yellow + @start_with.to_s.white
38
- puts ' Indent in Listing: '.yellow + @list_indent.to_s.white
39
- puts ' List Levels: '.yellow + @list_levels.to_s.white
40
- puts ' Debug? '.yellow + @debug.to_s.white
41
- puts ' Screen Lines: '.yellow + Gloo::App::Settings.lines( @engine ).to_s.white
42
- puts ' Page Size: '.yellow + Gloo::App::Settings.page_size( @engine ).to_s.white
39
+ theme = @engine.theme
40
+ puts theme.heading( "\n Application Settings:" )
41
+ puts theme.accent( ' Startup with: ' ) + theme.emphasis( @start_with.to_s )
42
+ puts theme.accent( ' Indent in Listing: ' ) + theme.emphasis( @list_indent.to_s )
43
+ puts theme.accent( ' List Levels: ' ) + theme.emphasis( @list_levels.to_s )
44
+ puts theme.accent( ' Debug? ' ) + theme.emphasis( @debug.to_s )
45
+ puts theme.accent( ' Theme: ' ) + theme.emphasis( @theme )
46
+ puts theme.accent( ' Screen Lines: ' ) + theme.emphasis( Gloo::App::Settings.lines( @engine ).to_s )
47
+ puts theme.accent( ' Page Size: ' ) + theme.emphasis( Gloo::App::Settings.page_size( @engine ).to_s )
43
48
  self.show_paths
44
49
  end
45
-
50
+
46
51
  #
47
52
  # Show path settings
48
53
  #
49
54
  def show_paths
50
- puts "\n Application Paths:".blue
51
- puts ' User Root Path is here: '.yellow + @user_root.white
52
- puts ' Projects Path: '.yellow + @project_path.white
53
- puts ' Extensions Path: '.yellow + @ext_path.white
54
- puts ' Tmp Path: '.yellow + @tmp_path.white
55
- puts ' Debug Path: '.yellow + @debug_path.white
55
+ theme = @engine.theme
56
+ puts theme.heading( "\n Application Paths:" )
57
+ puts theme.accent( ' User Root Path is here: ' ) + theme.emphasis( @user_root )
58
+ puts theme.accent( ' Projects Path: ' ) + theme.emphasis( @project_path )
59
+ puts theme.accent( ' Extensions Path: ' ) + theme.emphasis( @ext_path )
60
+ puts theme.accent( ' Tmp Path: ' ) + theme.emphasis( @tmp_path )
61
+ puts theme.accent( ' Debug Path: ' ) + theme.emphasis( @debug_path )
56
62
  puts "\n"
57
63
  end
58
64
 
@@ -154,6 +160,9 @@ module Gloo
154
160
  @list_levels = settings[ 'gloo' ][ 'list_levels' ]
155
161
 
156
162
  @debug = settings[ 'gloo' ][ 'debug' ]
163
+
164
+ @theme = settings[ 'gloo' ][ 'theme' ]&.strip&.downcase
165
+ @theme = DEFAULT_THEME unless THEMES.include?( @theme )
157
166
  end
158
167
 
159
168
  #
@@ -184,6 +193,7 @@ module Gloo
184
193
  list_indent: 2
185
194
  list_levels: 3
186
195
  debug: false
196
+ theme: #{DEFAULT_THEME}
187
197
  TEXT
188
198
  return str
189
199
  end
@@ -3,7 +3,6 @@
3
3
  #
4
4
  # CLI input.
5
5
  #
6
- require 'colorize'
7
6
  require 'terminal-table'
8
7
 
9
8
  module Gloo
@@ -28,14 +27,19 @@ module Gloo
28
27
  #
29
28
  # Show the given table data.
30
29
  #
30
+ # Deliberately left uncolored: box-drawing borders and text read
31
+ # fine against the terminal's own default colors, and forcing a
32
+ # fixed foreground/background (as this used to do) fought
33
+ # whatever theme the terminal was actually running.
34
+ #
31
35
  def show( headers, data, title = nil )
32
36
  unless title.blank?
33
- table = Terminal::Table.new(
37
+ table = Terminal::Table.new(
34
38
  :title => title, :headings => headers, :rows => data )
35
39
  else
36
40
  table = Terminal::Table.new( :headings => headers, :rows => data )
37
41
  end
38
- puts table.to_s.colorize( color: :white, background: :black )
42
+ puts table.to_s
39
43
  end
40
44
 
41
45
 
@@ -0,0 +1,144 @@
1
+ # Author:: Eric Crane (mailto:eric.crane@mac.com)
2
+ # Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
3
+ #
4
+ # Maps semantic color roles (heading, emphasis, etc) onto actual
5
+ # ANSI colors for the app's UI chrome (prompt, settings, log,
6
+ # tables, listings, help). Callers ask for a role, never a literal
7
+ # color, so a single palette swap (dark/light) fixes every call
8
+ # site at once.
9
+ #
10
+ # NOT used by the user-facing `show {target} (color)` verb or the
11
+ # `[colorize]` core-lib object - those are explicit color choices
12
+ # made in gloo scripts, not app chrome, and are intentionally left
13
+ # alone.
14
+ #
15
+ # Design note: most of the base 8 ANSI colors (red/green/yellow/
16
+ # blue/magenta/cyan) are exactly what a well-built terminal color
17
+ # scheme remaps per-background to stay legible - that's what
18
+ # switching a terminal profile from dark to light is for. `white`
19
+ # is the exception: by convention it stays "lightest foreground"
20
+ # regardless of background, so text forced to `.white` goes
21
+ # near-invisible on a light background no matter how good the
22
+ # terminal theme is. That's why :emphasis flips color between the
23
+ # two palettes below.
24
+ #
25
+ # `yellow` and `cyan` turned out to be further exceptions in
26
+ # practice: tested against a real light-background terminal, both
27
+ # were washed out and hard to read - the terminal's own theme
28
+ # wasn't remapping them as dark/saturated as hoped. So the light
29
+ # palette diverges from dark on more than just :emphasis, using
30
+ # explicit 256-color values (:ansi, raw SGR codes - none of these
31
+ # have a true equivalent in the base 16 ANSI colors) picked by eye
32
+ # against a real light terminal:
33
+ # - :warn uses orange instead of yellow
34
+ # - :accent uses a dark green instead of yellow
35
+ # - :subheading uses navy instead of cyan
36
+ #
37
+ require 'colorize'
38
+ require 'colorized_string'
39
+
40
+ module Gloo
41
+ module App
42
+ class Theme
43
+
44
+ # 256-color values picked by eye against a real light-background
45
+ # terminal - see design note above. 256-color support has been
46
+ # effectively universal in terminals for well over a decade.
47
+ ORANGE = '38;5;208'.freeze
48
+ NAVY = '38;5;18'.freeze
49
+ DARK_GREEN = '38;5;22'.freeze
50
+
51
+ #
52
+ # Dark-background palette. This matches the colors gloo
53
+ # has always used, so it's the default and non-breaking.
54
+ #
55
+ DARK = {
56
+ :heading => { :color => :blue, :mode => :bold },
57
+ :subheading => { :color => :cyan, :mode => :bold },
58
+ :accent => { :color => :yellow },
59
+ :emphasis => { :color => :white },
60
+ :muted => { :color => :light_black },
61
+ :warn => { :color => :yellow },
62
+ :error => { :color => :red }
63
+ }.freeze
64
+
65
+ #
66
+ # Light-background palette.
67
+ #
68
+ LIGHT = {
69
+ :heading => { :color => :blue, :mode => :bold },
70
+ :subheading => { :ansi => NAVY },
71
+ :accent => { :ansi => DARK_GREEN },
72
+ :emphasis => { :color => :black },
73
+ :muted => { :color => :light_black },
74
+ :warn => { :ansi => ORANGE },
75
+ :error => { :color => :red }
76
+ }.freeze
77
+
78
+ PALETTES = { 'dark' => DARK, 'light' => LIGHT }.freeze
79
+ ROLES = DARK.keys.freeze
80
+
81
+ attr_reader :name
82
+
83
+ #
84
+ # Set up the theme for the given theme name ('dark' or 'light').
85
+ # Falls back to the default (dark) for anything else.
86
+ #
87
+ def initialize( name = nil )
88
+ @name = PALETTES.key?( name ) ? name : Gloo::App::Settings::DEFAULT_THEME
89
+ @palette = PALETTES[ @name ]
90
+ end
91
+
92
+ #
93
+ # Build a Theme from the engine's settings.
94
+ #
95
+ def self.for_engine( engine )
96
+ return new( engine&.settings&.theme )
97
+ end
98
+
99
+ #
100
+ # Colorize the given string for the given semantic role.
101
+ # Unknown roles are returned unchanged rather than raising,
102
+ # so a typo'd role degrades to plain text instead of crashing.
103
+ #
104
+ # Named colors (:color/:mode) go through the colorize gem;
105
+ # :ansi is a raw SGR parameter string for colors the gem's
106
+ # named palette doesn't have (eg. 256-color orange).
107
+ #
108
+ def apply( str, role )
109
+ style = @palette[ role ]
110
+ return str.to_s unless style
111
+ return ansi_wrap( str.to_s, style ) if style[ :ansi ]
112
+
113
+ params = {}
114
+ params[ :color ] = style[ :color ] if style[ :color ]
115
+ params[ :mode ] = style[ :mode ] if style[ :mode ]
116
+ return ColorizedString[ str.to_s ].colorize( params ).to_s
117
+ end
118
+
119
+ #
120
+ # Define a convenience method per role, eg. theme.heading( str ).
121
+ #
122
+ ROLES.each do |role|
123
+ define_method( role ) do |str|
124
+ apply( str, role )
125
+ end
126
+ end
127
+
128
+ private
129
+
130
+ #
131
+ # Wrap a string in a raw SGR escape sequence - used for :ansi
132
+ # styles (eg. 256-color orange) that the colorize gem's named
133
+ # 16-color palette can't express.
134
+ #
135
+ def ansi_wrap( str, style )
136
+ codes = []
137
+ codes << String.mode_codes[ style[ :mode ] ] if style[ :mode ]
138
+ codes << style[ :ansi ]
139
+ return "\e[#{codes.join( ';' )}m#{str}\e[0m"
140
+ end
141
+
142
+ end
143
+ end
144
+ 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
+ # Resolves, validates and invokes a function. Shared by the
5
+ # `invoke`/`~>` verb (lib/gloo/verbs/invoke.rb) and inline calls
6
+ # inside any expression (lib/gloo/expr/call.rb), so both go through
7
+ # the same error handling instead of maintaining two copies of it.
8
+ #
9
+
10
+ module Gloo
11
+ module Core
12
+ class Invoker
13
+
14
+ NO_TARGET_ERR = 'Missing function reference!'.freeze
15
+ NOT_FOUND_ERR = 'Object was not found: '.freeze
16
+ NOT_FUNCTION_ERR = 'Not a function: '.freeze
17
+ PARAM_COUNT_ERR = 'Wrong number of parameters for function: '.freeze
18
+
19
+ #
20
+ # Resolve, validate and invoke the function at the given
21
+ # target path, with the given raw (unevaluated) arg tokens.
22
+ # Reports an error and returns nil for any failure - including
23
+ # a failure inside the function itself (see Function#invoke,
24
+ # which leaves `it` alone rather than setting it to an
25
+ # unreliable result when that happens).
26
+ #
27
+ def self.invoke( engine, target, arg_tokens )
28
+ if target.nil? || target.to_s.strip.empty?
29
+ engine.err NO_TARGET_ERR
30
+ return nil
31
+ end
32
+
33
+ func = resolve_function( engine, target )
34
+ return nil unless func
35
+
36
+ args = evaluate_arg_tokens( engine, arg_tokens )
37
+ return nil unless params_count_ok?( engine, func, args )
38
+
39
+ return invoke_function( engine, func, args )
40
+ end
41
+
42
+ #
43
+ # Resolve the function object at the target path. Reports an
44
+ # error and returns nil if the path doesn't resolve to
45
+ # anything, or resolves to an object that isn't a function.
46
+ #
47
+ def self.resolve_function( engine, target )
48
+ pn = Gloo::Core::Pn.new( engine, target )
49
+ func = pn.resolve
50
+
51
+ unless func
52
+ engine.err "#{NOT_FOUND_ERR}#{target}"
53
+ return nil
54
+ end
55
+
56
+ unless func.is_function?
57
+ engine.err "#{NOT_FUNCTION_ERR}#{target}"
58
+ return nil
59
+ end
60
+
61
+ return func
62
+ end
63
+
64
+ #
65
+ # Evaluate a list of raw tokens, each as its own single-token
66
+ # expression (a literal or object reference) - the standalone
67
+ # invoke verb's long-standing per-token semantics, now shared
68
+ # with the inline-call form too.
69
+ #
70
+ def self.evaluate_arg_tokens( engine, tokens )
71
+ return ( tokens || [] ).map do |t|
72
+ Gloo::Expr::Expression.new( engine, [ t ] ).evaluate
73
+ end
74
+ end
75
+
76
+ #
77
+ # Confirm the number of args given matches the number the
78
+ # function declares. Reports an error and returns false if
79
+ # they don't match.
80
+ #
81
+ def self.params_count_ok?( engine, func, args )
82
+ expected = func.params_hash&.keys&.length || 0
83
+ return true if args.count == expected
84
+
85
+ engine.err "#{PARAM_COUNT_ERR}#{func.pn} " \
86
+ "(expected #{expected}, got #{args.count})"
87
+ return false
88
+ end
89
+
90
+ #
91
+ # Invoke the function and return its result.
92
+ #
93
+ def self.invoke_function( engine, func, args )
94
+ engine.log.debug "invoking function: #{func.pn}"
95
+ result = func.invoke( args )
96
+ engine.log.debug "function returned: #{result}"
97
+ return result
98
+ end
99
+
100
+ end
101
+ end
102
+ end