inform-runtime 1.2.2 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +33 -41
  3. data/lib/story_teller/articles.rb +2 -1
  4. data/lib/story_teller/builtins.rb +63 -10
  5. data/lib/story_teller/color.rb +2 -1
  6. data/lib/story_teller/command.rb +4 -1
  7. data/lib/story_teller/context.rb +23 -27
  8. data/lib/story_teller/daemon.rb +2 -2
  9. data/lib/story_teller/engine.rb +80 -16
  10. data/lib/story_teller/events.rb +9 -2
  11. data/lib/story_teller/experimental/handler_dsl.rb +1 -16
  12. data/lib/story_teller/experimental/reverse_engineer_class.rb +1 -1
  13. data/lib/story_teller/grammar_parser.rb +60 -13
  14. data/lib/story_teller/helpers.rb +8 -18
  15. data/lib/story_teller/history.rb +2 -1
  16. data/lib/story_teller/inflector.rb +4 -2
  17. data/lib/story_teller/inform/ephemeral/link.rb +63 -31
  18. data/lib/story_teller/inform/ephemeral/module.rb +6 -5
  19. data/lib/story_teller/inform/ephemeral/object.rb +223 -236
  20. data/lib/story_teller/inform/ephemeral/tag.rb +27 -14
  21. data/lib/story_teller/io.rb +99 -49
  22. data/lib/story_teller/kernel.rb +13 -5
  23. data/lib/story_teller/library/bootstrap.rb +10 -27
  24. data/lib/story_teller/library/declarations.rb +1 -1
  25. data/lib/story_teller/library/directives.rb +1 -1
  26. data/lib/story_teller/library/loader.rb +4 -20
  27. data/lib/story_teller/library/location.rb +9 -3
  28. data/lib/story_teller/library.rb +1 -1
  29. data/lib/story_teller/logging.rb +165 -67
  30. data/lib/story_teller/mixins.rb +11 -4
  31. data/lib/story_teller/plurals.rb +2 -1
  32. data/lib/story_teller/privileges.rb +117 -0
  33. data/lib/story_teller/prototype.rb +158 -32
  34. data/lib/story_teller/publication.rb +2 -1
  35. data/lib/story_teller/session.rb +115 -25
  36. data/lib/story_teller/stdlib.rb +232 -2
  37. data/lib/story_teller/subscription.rb +2 -1
  38. data/lib/story_teller/tree.rb +2 -1
  39. data/lib/story_teller/version.rb +2 -2
  40. data/lib/story_teller/world_tree.rb +21 -23
  41. data/lib/story_teller.rb +18 -5
  42. metadata +7 -10
  43. data/lib/story_teller/core.rb +0 -38
  44. data/lib/story_teller/ephemeral_adapter.rb +0 -40
  45. data/lib/story_teller/inform/base.rb +0 -160
  46. data/lib/story_teller/model_adapter.rb +0 -132
@@ -2,7 +2,7 @@
2
2
  # encoding: utf-8
3
3
  # frozen_string_literal: false
4
4
 
5
- # Copyright Nels Nelson 2008-2025 but freely usable (see license)
5
+ # Copyright Nels Nelson 2008-2026 but freely usable (see license)
6
6
  #
7
7
  # This file is part of StoryTeller.
8
8
  #
@@ -26,26 +26,29 @@ if defined?(Java)
26
26
  require 'apache-log4j-2'
27
27
  end
28
28
 
29
- # The Logging module
30
- module Logging
29
+ # The LoggingConfiguration module
30
+ module LoggingConfiguration
31
+ # rubocop: disable Metrics/AbcSize
31
32
  # rubocop: disable Metrics/MethodLength
32
33
  def config
33
34
  @config ||= begin
34
- support_dir_path = File.expand_path(__dir__)
35
- lib_dir_path = File.expand_path(File.dirname(support_dir_path))
35
+ module_dir_path = File.expand_path(__dir__)
36
+ lib_dir_path = File.expand_path(File.dirname(module_dir_path))
36
37
  project_dir_path = File.expand_path(File.dirname(lib_dir_path))
37
38
  app_name = File.basename(project_dir_path)
38
39
  logs_dir_path = File.expand_path(File.join(project_dir_path, 'logs'))
39
40
  FileUtils.mkdir_p(logs_dir_path)
40
- log_file = File.expand_path(File.join(logs_dir_path, "#{app_name}.log"))
41
+ log_file_path = File.expand_path(File.join(logs_dir_path, "#{app_name}.log"))
42
+ FileUtils.touch(log_file_path)
41
43
  {
42
- level: :info,
44
+ level: ENV['INFORM_LOG_LEVEL']&.to_i || Logger::INFO,
43
45
  name: app_name,
44
46
  lib_dir_path: lib_dir_path,
45
47
  project_dir_path: project_dir_path,
46
48
  logs_dir_path: logs_dir_path,
47
- log_file: log_file,
48
- rolling_log_file_name_template: "#{app_name}-%d{yyyy-MM-dd}.log.gz",
49
+ log_file_path: log_file_path,
50
+ rolling_log_file_name_template: File.expand_path(
51
+ File.join(logs_dir_path, "#{app_name}-%d{yyyy-MM-dd}.log.gz")),
49
52
  logging_timestamp_format: '%Y-%m-%d %H:%M:%S',
50
53
  logging_pattern_template: {
51
54
  java: '%d{ABSOLUTE} %-5p [%c{1}] %m%n',
@@ -57,103 +60,182 @@ module Logging
57
60
  end
58
61
  end
59
62
  module_function :config
63
+ # rubocop: enable Metrics/AbcSize
64
+ # rubocop: enable Metrics/MethodLength
65
+ end
66
+
67
+ # The LoggingHelpers module
68
+ module LoggingHelpers
69
+ if defined?(Java)
70
+ java_import Java::org.apache.logging.log4j.core.appender::ConsoleAppender
71
+ java_import Java::org.apache.logging.log4j.core.config::Configurator
72
+ java_import Java::org.apache.logging.log4j.core.config.builder.api::ConfigurationBuilderFactory
73
+ java_import Java::org.apache.logging.log4j::Level
74
+ end
75
+
76
+ # rubocop: disable Metrics/AbcSize
77
+ # rubocop: disable Metrics/MethodLength
78
+ def init_log4j(log_level = org.apache.logging.log4j.Level::INFO)
79
+ java.lang::System.setProperty('log4j.shutdownHookEnabled', java.lang::Boolean.toString(false))
80
+ config = ConfigurationBuilderFactory.newConfigurationBuilder()
81
+
82
+ log_level = Level.to_level(log_level.to_s.upcase) if log_level.is_a? Symbol
83
+ config.setStatusLevel(log_level)
84
+ config.setConfigurationName(LoggingConfiguration.config[:name])
85
+
86
+ # create a console appender
87
+ target = ConsoleAppender::Target::SYSTEM_OUT
88
+ pattern = LoggingConfiguration.config[:logging_pattern_template][:java]
89
+ layout = config.newLayout('PatternLayout')
90
+ layout = layout.addAttribute('pattern', pattern)
91
+ appender = config.newAppender('stdout', 'CONSOLE')
92
+ appender = appender.addAttribute('target', target)
93
+ appender = appender.add(layout)
94
+ config.add(appender)
95
+
96
+ # create a root logger
97
+ root_logger = config.newRootLogger(log_level)
98
+ root_logger = root_logger.add(config.newAppenderRef('stdout'))
99
+
100
+ # create a rolling file appender
101
+ cron = config.newComponent('CronTriggeringPolicy')
102
+ cron = cron.addAttribute('schedule', '0 0 0 * * ?')
103
+
104
+ size = config.newComponent('SizeBasedTriggeringPolicy')
105
+ size = size.addAttribute('size', LoggingConfiguration.config[:size])
106
+
107
+ policies = config.newComponent('Policies')
108
+ policies = policies.addComponent(cron)
109
+ policies = policies.addComponent(size)
110
+
111
+ appender = config.newAppender('rolling_file', 'RollingFile')
112
+ appender = appender.addAttribute(
113
+ 'fileName', LoggingConfiguration.config[:log_file_path])
114
+ appender = appender.addAttribute(
115
+ 'filePattern', LoggingConfiguration.config[:rolling_log_file_name_template])
116
+ appender = appender.add(layout)
117
+ appender = appender.addComponent(policies)
118
+ config.add(appender)
119
+
120
+ root_logger = root_logger.addAttribute('additivity', false)
121
+ root_logger = root_logger.add(config.newAppenderRef('rolling_file'))
122
+ config.add(root_logger)
123
+
124
+ logging_configuration = config.build()
125
+ ctx = Configurator.initialize(logging_configuration)
126
+ ctx.updateLoggers()
127
+ end
128
+ module_function :init_log4j
129
+ # rubocop: enable Metrics/AbcSize
60
130
  # rubocop: enable Metrics/MethodLength
131
+ # def init_log4j
132
+
133
+ LoggingHelpers.init_log4j if defined?(Java)
61
134
  end
135
+ # module Logging
62
136
 
63
137
  # Namespace for methods to help with implicit backtrace printing
64
- module LoggerHelpers
138
+ module TraceHelpers
65
139
  def generate_message(error_or_message, error)
66
140
  error_message = "#{error_or_message}: #{error.class.name}"
67
141
  error_message << ": #{error.message}" if error.respond_to?(:message)
68
142
  error_message
69
143
  end
144
+ module_function :generate_message
70
145
 
71
146
  def extract_backtrace(error, default_result = nil)
72
147
  if error.respond_to?(:backtrace)
73
- error.backtrace.each { |trace| original_error(trace) unless trace.nil? }
148
+ error.backtrace || default_result
74
149
  elsif error.respond_to?(:getStackTrace)
75
- error.getStackTrace().each { |trace| original_error(trace) unless trace.nil? }
150
+ error.getStackTrace() || default_result
76
151
  else
77
152
  default_result
78
153
  end
79
154
  end
155
+ module_function :extract_backtrace
80
156
  end
81
157
 
82
- # Monkey-patch the built-in Ruby Logger class to support
83
- # implicit backtrace printing
84
- # TODO: Figure out if this is actually useful.
158
+ # class Logger
85
159
  class Logger
86
- TRACE = DEBUG - 1 unless const_defined?(:TRACE)
87
-
88
- include LoggerHelpers
160
+ # module Severity
161
+ module Severity
162
+ # Lowest-level information, for developers.
163
+ TRACE = -1
164
+ LEVELS['trace'] = Logger::Severity::TRACE
165
+ end
166
+ end
89
167
 
168
+ # Add extra severity to the standard Logger class.
169
+ # Also add support for including backtraces implicitly.
170
+ class ExtraLogger < Logger
90
171
  alias original_error error
172
+ def error(error_or_message = nil, error = nil, &block)
173
+ return log_error(error_or_message, &block) if error.nil?
91
174
 
92
- def error(error_or_message, error = nil)
93
- warn error_or_message
94
- return extract_backtrace(error_or_message) if error.nil?
95
- original_error(generate_message(error_or_message, error))
96
- extract_backtrace(original_error(error))
175
+ original_error(TraceHelpers.generate_message(error_or_message, error))
176
+ TraceHelpers.extract_backtrace(error, []).each do |trace|
177
+ original_error(trace) unless trace.nil?
178
+ end
97
179
  end
98
180
 
99
- def trace(progname = nil, &block)
100
- add(TRACE, nil, progname, &block)
181
+ def log_error(error_or_message = nil, &block)
182
+ original_error(error_or_message, &block)
183
+ TraceHelpers.extract_backtrace(error_or_message, []).each do |trace|
184
+ original_error(trace) unless trace.nil?
185
+ end
101
186
  end
102
187
 
103
- def trace?
104
- @level <= TRACE
188
+ SEV_LABEL_EXTRA = (Logger::SEV_LABEL + %w(TRACE)).freeze
189
+ def format_severity(severity)
190
+ SEV_LABEL_EXTRA[severity] || super
191
+ end
192
+
193
+ def trace(progname = nil, &block)
194
+ add(Logger::Severity::TRACE, nil, progname, &block)
105
195
  end
106
196
  end
107
197
 
108
- # The Logging module
109
- module Logging
198
+ # The LoggingHelpers module
199
+ module LoggingHelpers
110
200
  if defined?(Java)
111
201
  java_import Java::org.apache.logging.log4j.Level
112
202
  java_import Java::org.apache.logging.log4j.LogManager
113
203
  end
114
204
 
115
- RubyLogLevels = {
116
- all: -Float::INFINITY,
117
- trace: Logger::TRACE,
118
- debug: Logger::DEBUG,
119
- info: Logger::INFO,
120
- warn: Logger::WARN,
121
- error: Logger::ERROR,
122
- fatal: Logger::FATAL,
123
- unknown: Logger::UNKNOWN,
124
- off: Logger::UNKNOWN + 1
125
- }.freeze
205
+ FORWARD_SLASH_PATTERN = %r{/} unless defined?(FORWARD_SLASH_PATTERN)
126
206
 
127
- def init_logger(level = :info, logger_name = nil)
128
- return init_java_logger(level, logger_name, caller[2]) if defined?(Java)
129
- init_ruby_logger(level, logger_name, caller[2])
207
+ def init_logger(obj, level = :info, logger_name = nil)
208
+ return init_java_logger(obj, level, logger_name, caller[2]) if defined?(Java)
209
+ init_ruby_logger(obj, level, logger_name, caller[2])
130
210
  end
211
+ module_function :init_logger
131
212
 
132
- def ruby_log_formatter(severity, datetime, progname, message)
213
+ def ruby_log_formatter(severity_level, datetime, program_name, message)
133
214
  format(
134
- Logging.config[:logging_pattern_template][:ruby],
135
- timestamp: datetime.strftime(Logging.config[:logging_timestamp_format]),
136
- progname: progname, severity: severity, msg: message)
215
+ LoggingConfiguration.config[:logging_pattern_template][:ruby],
216
+ timestamp: datetime.strftime(LoggingConfiguration.config[:logging_timestamp_format]),
217
+ progname: program_name, severity: severity_level, msg: message)
137
218
  end
219
+ module_function :ruby_log_formatter
138
220
 
139
- ForwardSlashPattern = %r{/}.freeze
140
-
141
- def init_ruby_logger(level = nil, logger_name = nil, source_location = nil)
142
- logger_name = get_formatted_logger_name(logger_name)
143
- logger_name = source_location.split(ForwardSlashPattern).last if logger_name.empty?
144
- log = Logger.new($stdout, progname: logger_name)
221
+ def init_ruby_logger(obj, level = nil, logger_name = nil, source_location = nil)
222
+ logger_name = get_formatted_logger_name(obj, logger_name)
223
+ logger_name = source_location.split(FORWARD_SLASH_PATTERN).last if logger_name.empty?
224
+ log = ExtraLogger.new($stdout, progname: logger_name)
145
225
  log.level = normalize_ruby_log_level(level) unless level.nil?
146
226
  log.formatter = method(:ruby_log_formatter)
147
227
  log
148
228
  end
229
+ module_function :init_ruby_logger
149
230
 
150
- def init_java_logger(level = nil, logger_name = nil, source_location = nil)
151
- logger_name = get_formatted_logger_name(logger_name)
152
- logger_name = source_location.split(ForwardSlashPattern).last if logger_name.empty?
231
+ def init_java_logger(obj, level = nil, logger_name = nil, source_location = nil)
232
+ logger_name = get_formatted_logger_name(obj, logger_name)
233
+ logger_name = source_location.split(FORWARD_SLASH_PATTERN).last if logger_name.empty?
153
234
  log = LogManager.getLogger(logger_name)
154
235
  log.level = Level.to_level(level.to_s.upcase) unless level.nil?
155
236
  log
156
237
  end
238
+ module_function :init_java_logger
157
239
 
158
240
  def normalize_ruby_log_level(level)
159
241
  return level if level.is_a?(Integer)
@@ -162,12 +244,26 @@ module Logging
162
244
  raise ArgumentError, "Unknown Ruby log level: #{level.inspect}"
163
245
  end
164
246
  end
247
+ module_function :normalize_ruby_log_level
165
248
 
166
- def get_formatted_logger_name(logger_name = nil)
167
- return logger_name.to_s[/\w+$/] unless logger_name.nil?
168
- return name[/\w+$/] if is_a?(Class) || is_a?(Module)
169
- self.class.name[/\w+$/]
249
+ def get_formatted_logger_name(obj, logger_name = nil)
250
+ return logger_name.to_s if !logger_name.nil? && logger_name.respond_to?(:to_s)
251
+ return obj.name if obj.is_a?(Class) || obj.is_a?(Module)
252
+ obj.class.name
170
253
  end
254
+ module_function :get_formatted_logger_name
255
+
256
+ RubyLogLevels = {
257
+ all: -Float::INFINITY,
258
+ trace: Logger::TRACE,
259
+ debug: Logger::DEBUG,
260
+ info: Logger::INFO,
261
+ warn: Logger::WARN,
262
+ error: Logger::ERROR,
263
+ fatal: Logger::FATAL,
264
+ unknown: Logger::UNKNOWN,
265
+ off: Logger::UNKNOWN + 1
266
+ }.freeze
171
267
 
172
268
  # rubocop: disable Metrics/CyclomaticComplexity
173
269
  # OFF: 0
@@ -191,23 +287,25 @@ module Logging
191
287
  when -Float::INFINITY..-2 then :all
192
288
  end
193
289
  end
290
+ module_function :symbolize_numeric_log_level
194
291
  # rubocop: enable Metrics/CyclomaticComplexity
292
+ end
293
+ # module LoggingHelpers
195
294
 
295
+ # The Logging module
296
+ module Logging
196
297
  def log_level=(level)
197
- raise "Parameter level may not be nil" if level.nil?
198
- normalized_level = symbolize_numeric_log_level(level)
199
-
200
- Logging.config[:level] = normalized_level
298
+ LoggingConfiguration.config[:level] = LoggingHelpers.symbolize_numeric_log_level(level)
201
299
  end
202
300
  module_function :log_level=
203
301
 
204
302
  def log_level
205
- Logging.config[:level]
303
+ LoggingConfiguration.config[:level]
206
304
  end
207
305
  module_function :log_level
208
306
 
209
- def log(level = Logging.log_level, log_name = Logging.config[:app_name])
210
- @log ||= init_logger(level, log_name)
307
+ def log(level = Logging.log_level, log_name = LoggingConfiguration.config[:app_name])
308
+ @log ||= LoggingHelpers.init_logger(self, level, log_name)
211
309
  end
212
310
  alias logger log
213
311
  end
@@ -1,7 +1,8 @@
1
+ # lib/story_teller/mixins.rb
1
2
  # encoding: utf-8
2
3
  # frozen_string_literal: false
3
4
 
4
- # Copyright Nels Nelson 2008-2025 but freely usable (see license)
5
+ # Copyright Nels Nelson 2008-2026 but freely usable (see license)
5
6
  #
6
7
  # This file is part of StoryTeller.
7
8
  #
@@ -456,9 +457,15 @@ class Array
456
457
  end
457
458
  end
458
459
 
459
- def sum(&block)
460
- return inject(0, &block) if block_given?
461
- inject(:+)
460
+ def sum(init = nil)
461
+ if block_given?
462
+ initial = init.nil? ? 0 : init
463
+ inject(initial) { |total, element| total + yield(element) }
464
+ elsif init.nil?
465
+ inject(:+)
466
+ else
467
+ inject(init, :+)
468
+ end
462
469
  end
463
470
 
464
471
  def average
@@ -1,7 +1,8 @@
1
+ # lib/story_teller/plurals.rb
1
2
  # encoding: utf-8
2
3
  # frozen_string_literal: false
3
4
 
4
- # Copyright Nels Nelson 2008-2025 but freely usable (see license)
5
+ # Copyright Nels Nelson 2008-2026 but freely usable (see license)
5
6
  #
6
7
  # This file is part of the StoryTeller.
7
8
  #
@@ -0,0 +1,117 @@
1
+ # lib/story_teller/privileges.rb
2
+ # encoding: utf-8
3
+ # frozen_string_literal: false
4
+
5
+ # Copyright Nels Nelson 2008-2026 but freely usable (see license)
6
+ #
7
+ # This file is part of the StoryTeller.
8
+ #
9
+ # The StoryTeller is free software: you can redistribute it and/or
10
+ # modify it under the terms of the GNU General Public License as published
11
+ # by the Free Software Foundation, either version 3 of the License, or
12
+ # (at your option) any later version.
13
+ #
14
+ # The StoryTeller is distributed in the hope that it will be useful,
15
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ # GNU General Public License for more details.
18
+ #
19
+ # You should have received a copy of the GNU General Public License
20
+ # along with the StoryTeller. If not, see <http://www.gnu.org/licenses/>.
21
+
22
+ # module StoryTeller
23
+ module StoryTeller
24
+ # module PrivilegedIdentity
25
+ module PrivilegedIdentity
26
+ def privilege?(_privilege)
27
+ false
28
+ end
29
+
30
+ def privilege_label
31
+ to_s
32
+ end
33
+ end
34
+
35
+ # The StoryTeller::Privileges module to implement privilege
36
+ module Privileges
37
+ class << self
38
+ def privileged?(subject, privilege)
39
+ StoryTeller::PrivilegeGrants.granted?(subject, privilege)
40
+ end
41
+
42
+ def subject_for(subject)
43
+ session_for(subject) || subject
44
+ end
45
+
46
+ private
47
+
48
+ def session_for(subject)
49
+ return nil if subject.nil?
50
+ return nil unless defined?(StoryTeller::IO::Session)
51
+ return subject if subject.is_a?(StoryTeller::IO::Session)
52
+ return subject.instance_variable_get(:@session) if subject.instance_variable_defined?(:@session)
53
+
54
+ StoryTeller::IO::Session.of(subject)
55
+ end
56
+ end
57
+
58
+ def privileged?(privilege, subject = self)
59
+ StoryTeller::Privileges.privileged?(subject, privilege)
60
+ end
61
+ end
62
+
63
+ # module PrivilegeGrants
64
+ module PrivilegeGrants
65
+ @session_grants = Hash.new { |hash, key| hash[key] = Set.new }
66
+ @mode = :persistent
67
+
68
+ class << self
69
+ attr_accessor :mode
70
+
71
+ def grant_session(subject, privilege)
72
+ subject = StoryTeller::Privileges.subject_for(subject)
73
+ return nil if subject.nil?
74
+
75
+ @session_grants[identity(subject)].add(privilege.to_sym)
76
+ end
77
+
78
+ def session_granted?(subject, privilege)
79
+ subject = StoryTeller::Privileges.subject_for(subject)
80
+ return false if subject.nil?
81
+
82
+ @session_grants[identity(subject)].include?(privilege.to_sym)
83
+ end
84
+
85
+ def persistent_granted?(subject, privilege)
86
+ subject = StoryTeller::Privileges.subject_for(subject)
87
+ return false unless subject.is_a?(StoryTeller::PrivilegedIdentity)
88
+
89
+ subject.privilege?(privilege.to_sym)
90
+ end
91
+
92
+ def granted?(subject, privilege)
93
+ privilege = privilege.to_sym
94
+
95
+ session_granted?(subject, privilege) ||
96
+ (privilege == :builder && session_granted?(subject, :admin)) ||
97
+ persistent_granted_under_mode?(subject, privilege)
98
+ end
99
+
100
+ private
101
+
102
+ def persistent_granted_under_mode?(subject, privilege)
103
+ return false if mode == :session_only
104
+
105
+ persistent_granted?(subject, privilege) ||
106
+ (privilege == :builder && persistent_granted?(subject, :admin))
107
+ end
108
+
109
+ def identity(subject)
110
+ return subject.identity if subject.respond_to?(:identity)
111
+
112
+ subject.object_id
113
+ end
114
+ end
115
+ end
116
+ end
117
+ # module StoryTeller