inform-runtime 1.0.4

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 (59) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +623 -0
  3. data/README.md +185 -0
  4. data/Rakefile +65 -0
  5. data/config/database.yml +37 -0
  6. data/exe/inform.rb +6 -0
  7. data/game/config.yml +5 -0
  8. data/game/example.inf +76 -0
  9. data/game/example.rb +90 -0
  10. data/game/forms/example_form.rb +2 -0
  11. data/game/grammar/game_grammar.inf.rb +11 -0
  12. data/game/languages/english.rb +2 -0
  13. data/game/models/example_model.rb +2 -0
  14. data/game/modules/example_module.rb +9 -0
  15. data/game/rules/example_state.rb +2 -0
  16. data/game/scripts/example_script.rb +2 -0
  17. data/game/topics/example_topic.rb +2 -0
  18. data/game/verbs/game_verbs.rb +15 -0
  19. data/game/verbs/metaverbs.rb +2028 -0
  20. data/lib/runtime/articles.rb +138 -0
  21. data/lib/runtime/builtins.rb +359 -0
  22. data/lib/runtime/color.rb +145 -0
  23. data/lib/runtime/command.rb +470 -0
  24. data/lib/runtime/config.rb +48 -0
  25. data/lib/runtime/context.rb +78 -0
  26. data/lib/runtime/daemon.rb +266 -0
  27. data/lib/runtime/database.rb +500 -0
  28. data/lib/runtime/events.rb +771 -0
  29. data/lib/runtime/experimental/handler_dsl.rb +175 -0
  30. data/lib/runtime/game.rb +74 -0
  31. data/lib/runtime/game_loader.rb +132 -0
  32. data/lib/runtime/grammar_parser.rb +553 -0
  33. data/lib/runtime/helpers.rb +177 -0
  34. data/lib/runtime/history.rb +45 -0
  35. data/lib/runtime/inflector.rb +195 -0
  36. data/lib/runtime/io.rb +174 -0
  37. data/lib/runtime/kernel.rb +450 -0
  38. data/lib/runtime/library.rb +59 -0
  39. data/lib/runtime/library_loader.rb +135 -0
  40. data/lib/runtime/link.rb +158 -0
  41. data/lib/runtime/logging.rb +197 -0
  42. data/lib/runtime/mixins.rb +570 -0
  43. data/lib/runtime/module.rb +202 -0
  44. data/lib/runtime/object.rb +761 -0
  45. data/lib/runtime/options.rb +104 -0
  46. data/lib/runtime/persistence.rb +292 -0
  47. data/lib/runtime/plurals.rb +60 -0
  48. data/lib/runtime/prototype.rb +307 -0
  49. data/lib/runtime/publication.rb +92 -0
  50. data/lib/runtime/runtime.rb +321 -0
  51. data/lib/runtime/session.rb +202 -0
  52. data/lib/runtime/stdlib.rb +604 -0
  53. data/lib/runtime/subscription.rb +47 -0
  54. data/lib/runtime/tag.rb +287 -0
  55. data/lib/runtime/tree.rb +204 -0
  56. data/lib/runtime/version.rb +24 -0
  57. data/lib/runtime/world_tree.rb +69 -0
  58. data/lib/runtime.rb +35 -0
  59. metadata +199 -0
@@ -0,0 +1,500 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: false
3
+
4
+ # Copyright Nels Nelson 2008-2023 but freely usable (see license)
5
+ #
6
+ # This file is part of the Inform Runtime.
7
+ #
8
+ # The Inform Runtime is free software: you can redistribute it and/or
9
+ # modify it under the terms of the GNU General Public License as published
10
+ # by the Free Software Foundation, either version 3 of the License, or
11
+ # (at your option) any later version.
12
+ #
13
+ # The Inform Runtime is distributed in the hope that it will be useful,
14
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
+ # GNU General Public License for more details.
17
+ #
18
+ # You should have received a copy of the GNU General Public License
19
+ # along with the Inform Runtime. If not, see <http://www.gnu.org/licenses/>.
20
+
21
+ require 'sequel'
22
+
23
+ # The Sequel module
24
+ module Sequel
25
+ # The Sequel::Migration class
26
+ class Migration
27
+ def self.up
28
+ self.apply(Sequel::Model.db, :up)
29
+ end
30
+ def self.down
31
+ self.apply(Sequel::Model.db, :down)
32
+ end
33
+ def self.table_exists?(table)
34
+ Sequel::Model.db.table_exists?(table)
35
+ end
36
+ end
37
+ end
38
+
39
+ # The Databases module
40
+ module Databases
41
+ # The Databases::Helpers module
42
+ module Helpers
43
+ MigrationMethods = %i[down up].freeze
44
+ NamespaceDelimiterPattern = /::/.freeze
45
+ DatabasePattern = /Database/.freeze
46
+
47
+ def class_derived_database_name
48
+ self.class.name.split(NamespaceDelimiterPattern).last.sub(DatabasePattern, '')
49
+ end
50
+
51
+ def implicit_database_name
52
+ implicit_database_name = self.class.name
53
+ begin
54
+ implicit_database_name = underscore(demodulize(class_derived_database_name)).to_sym
55
+ rescue StandardError => e
56
+ log.warn "Failed to get implicit database name: #{e.message}"
57
+ end
58
+ implicit_database_name
59
+ end
60
+
61
+ def database
62
+ # implicit_database_name
63
+ underscore(Inform::Databases.instances.keys.first).to_sym
64
+ end
65
+
66
+ def execute(sql)
67
+ Sequel::Model.db[sql]
68
+ end
69
+
70
+ def run(sql)
71
+ Sequel::Model.db.run(sql)
72
+ end
73
+
74
+ def database_exist?(database_name = database)
75
+ result = execute("select count(*) > 0 from pg_catalog.pg_database where datname = '#{database_name}';")&.first
76
+ result&.values&.first == true
77
+ end
78
+
79
+ def user_exist?(username = database)
80
+ result = execute("select count(*) > 0 from pg_shadow where usename = '#{username}';")&.first
81
+ result&.values&.first == true
82
+ end
83
+
84
+ def connected_database
85
+ return nil unless Sequel::Model.db
86
+ Sequel::Model.db.opts[:uri].to_s.scan(/:\/\/\w+(:\d+|\/)(\w+)/).last
87
+ end
88
+
89
+ def connected_username
90
+ return nil unless Sequel::Model.db
91
+ Sequel::Model.db.opts[:uri].to_s.scan(/user=(\w+)/).first
92
+ end
93
+ end
94
+ end
95
+
96
+ if defined? GlobalSequelIdentityMap
97
+ # The Sequel module
98
+ module Sequel
99
+ # The Sequel::Plugins module
100
+ module Plugins
101
+ # The Sequel::Plugins::IdentityMap module
102
+ module IdentityMap
103
+ # The Sequel::Plugins::IdentityMap::ClassMethods module
104
+ module ClassMethods
105
+ # Returns the global thread-safe identity map.
106
+ def identity_map
107
+ log.debug "Getting identity map"
108
+ GlobalSequelIdentityMap
109
+ end
110
+
111
+ private
112
+
113
+ # Set the thread local identity map to the given value.
114
+ def identity_map=(v)
115
+ silence_warnings do
116
+ log.warn "Setting identity map"
117
+ Object.instance_eval { const_set :GlobalSequelIdentityMap, v }
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+
126
+ # The DatabaseConnectionHelpers module
127
+ module DatabaseConnectionHelpers
128
+ ConnectionAttempts = Struct.new(:memo).new(0)
129
+ DefaultConnectionOptions = { loggerLevel: 'OFF' }.freeze
130
+ DefaultConnectionPoolSize = 4
131
+ DefaultConnectionValidationTimeoutSeconds = 240
132
+ URLTemplate = '%<scheme>s://%<host>s%<colon>s%<port>s'.freeze
133
+ KeyValueTemplate = '%<key>s=%<value>s'.freeze
134
+ EmptyString = ''.freeze
135
+ AmpersandString = '&'.freeze
136
+ ColonString = ':'.freeze
137
+ ForwardSlashString = '/'.freeze
138
+ QuestionMarkString = '?'.freeze
139
+ JavaPattern = /java/i.freeze
140
+
141
+ # Previously:
142
+ # url = "#{adapter}://#{host}/#{database}?user=#{username}"
143
+ # url << "&password=#{password}" unless password.nil? || password.empty?
144
+ # url << "&loggerLevel=OFF"
145
+ # rubocop: disable Metrics/AbcSize
146
+ # rubocop: disable Metrics/CyclomaticComplexity
147
+ # rubocop: disable Metrics/MethodLength
148
+ # rubocop: disable Metrics/PerceivedComplexity
149
+ def assemble_url
150
+ adapter = config.fetch('adapter')
151
+ adapter.gsub!(/^postgres/, 'jdbc:postgresql') if JavaPattern.match?(RUBY_PLATFORM)
152
+ host = config.fetch('host', nil)
153
+ port = config.fetch('port', nil)
154
+ colon = port.nil? ? EmptyString : ColonString
155
+ database = config.fetch('database', nil)
156
+ username = config.fetch('username', nil)
157
+ password = config.fetch('password', nil)
158
+ parameters = DefaultConnectionOptions.dup
159
+ parameters.merge!(user: username) unless username.nil?
160
+ parameters.merge!(password: password) unless password.nil? || password.empty?
161
+ parameter_values = parameters.map { |key, value| format(KeyValueTemplate, key: key, value: value) }
162
+ query = parameter_values.join(AmpersandString)
163
+ url = [format(URLTemplate, scheme: adapter, host: host, colon: colon, port: port)]
164
+ url << database unless database.nil? || database.empty?
165
+ url = [url.join(ForwardSlashString)]
166
+ url << query unless query.nil? || query.empty?
167
+ url.join(QuestionMarkString)
168
+ end
169
+ # rubocop: enable Metrics/AbcSize
170
+ # rubocop: enable Metrics/CyclomaticComplexity
171
+ # rubocop: enable Metrics/MethodLength
172
+ # rubocop: enable Metrics/PerceivedComplexity
173
+
174
+ DatabaseOrRoleDoesNotExistPattern = /(database|role) ".*" does not exist/.freeze
175
+
176
+ # rubocop: disable Metrics/AbcSize
177
+ # rubocop: disable Metrics/MethodLength
178
+ def connect(database = config.fetch('database', nil), url = assemble_url, connection_options = {})
179
+ log.debug "Connecting to database #{sanitize_url(url)}" if defined? log
180
+ ConnectionAttempts.memo += 1
181
+ connection_options[:max_connections] = config.fetch('pool', DefaultConnectionPoolSize)
182
+ connection = Sequel.connect(url, **connection_options)
183
+ connection.extension(:connection_validator)
184
+ connection.pool.connection_validation_timeout = DefaultConnectionValidationTimeoutSeconds
185
+ connection.test_connection
186
+ log.debug "Connected to database #{database}" if defined? log
187
+ Sequel::Model.require_valid_table = false
188
+ Sequel::Model.db = connection
189
+ rescue Sequel::DatabaseConnectionError => e
190
+ raise if e.cause.is_a?(PG::ConnectionBad) && DatabaseOrRoleDoesNotExistPattern.match?(e.cause.to_s)
191
+ error_message = e.message.gsub(/Java::OrgPostgresqlUtil::PSQLException: /, '')
192
+ error_message = "Error connecting to database: #{error_message}"
193
+ log.error error_message
194
+ abort
195
+ rescue Sequel::AdapterNotFound => e
196
+ error_message = "Adapter not found: #{e.message}"
197
+ log.error error_message
198
+ abort
199
+ rescue StandardError => e
200
+ error_message = "Unexpected error connecting to database: #{e.message}"
201
+ log.error error_message, e
202
+ abort
203
+ end
204
+ # rubocop: enable Metrics/AbcSize
205
+ # rubocop: enable Metrics/MethodLength
206
+
207
+ def sanitize_url(url)
208
+ url.gsub(/&?(password|loggerLevel)=[^&]+&?/, '').gsub(/\?$/, '')
209
+ end
210
+ end
211
+ # module DatabaseConnectionHelpers
212
+
213
+ # The Databases module
214
+ module Databases
215
+ # The Databases::Management module
216
+ module Management
217
+ include DatabaseConnectionHelpers
218
+
219
+ # rubocop: disable Metrics/AbcSize
220
+ # rubocop: disable Metrics/MethodLength
221
+ def reconnect_database(url)
222
+ log.debug "Reconnecting to database #{sanitize_url(url)}"
223
+ begin
224
+ Sequel::Model.db&.disconnect
225
+ rescue StandardError => e
226
+ log.warn "Encountered unexpected error disconnecting from database: #{e.message}"
227
+ end
228
+ Sequel::Model.db = Sequel.connect(url)
229
+ Sequel::Model.descendants.each do |model|
230
+ model_dataset = begin
231
+ model.dataset
232
+ rescue Sequel::Error => e
233
+ nil
234
+ end
235
+ model.db = Sequel::Model.db if model_dataset.nil?
236
+ end
237
+ end
238
+ # rubocop: enable Metrics/AbcSize
239
+ # rubocop: enable Metrics/MethodLength
240
+
241
+ # rubocop: disable Metrics/AbcSize
242
+ # rubocop: disable Metrics/MethodLength
243
+ def create_database(database_name = database, username = database)
244
+ return false if database_exist?(database_name)
245
+ begin
246
+ log.info "Creating database: #{database_name}"
247
+ create_user username unless user_exist?(username)
248
+ run "create database #{database_name} owner #{username}"
249
+ rescue StandardError => e
250
+ database_already_exists_error_pattern = /ERROR: database "#{database_name}" already exists/
251
+ if database_already_exists_error_pattern.match?(e.to_s)
252
+ unless defined? tried_once_already
253
+ drop_database database_name
254
+ tried_once_already = true # rubocop: disable Lint/UselessAssignment
255
+ retry
256
+ end
257
+ end
258
+ log.error "Unexpected error creating database: #{e.inspect}"
259
+ e.backtrace.each { |t| log.error t }
260
+ return false
261
+ end
262
+ end
263
+ # rubocop: enable Metrics/AbcSize
264
+ # rubocop: enable Metrics/MethodLength
265
+
266
+ CannotDropOpenDatabaseErrorPattern = /ERROR: cannot drop the currently open database/.freeze
267
+
268
+ # rubocop: disable Metrics/AbcSize
269
+ # rubocop: disable Metrics/MethodLength
270
+ def drop_database(database_name = database)
271
+ return false unless database_exist?(database_name)
272
+ begin
273
+ log.info "Dropping database: #{database_name}"
274
+ run "drop database #{database_name}"
275
+ rescue Sequel::DatabaseError => e
276
+ log.error "Unexpected error creating database: #{e.inspect}"
277
+ e.backtrace.each { |t| log.error t }
278
+ return false
279
+ rescue StandardError => e
280
+ if CannotDropOpenDatabaseErrorPattern.match?(e.to_s)
281
+ results = execute "select procpid from pg_stat_activity where datname = '#{database_name}'"
282
+ results.each do |row|
283
+ log.debug "You must first kill process with pid #{row['procpid']}"
284
+ command = format('sudo kill -9 %<procpid>s', procpid: row['procpid'])
285
+ log.debug "Execute: #{command}"
286
+ # `#{command}`
287
+ end
288
+ end
289
+ log.error "Unexpected error creating database: #{e.inspect}"
290
+ e.backtrace.each { |t| log.error t }
291
+ return false
292
+ end
293
+ end
294
+ # rubocop: enable Metrics/AbcSize
295
+ # rubocop: enable Metrics/MethodLength
296
+ end
297
+ # module Management
298
+ end
299
+ # module Databases
300
+
301
+ # The Databases module
302
+ module Databases
303
+ # The Utilities module
304
+ module Utilities
305
+ DatabaseURITemplate = '%<schema>s://%<host>s%<port>s/%<database_name>s?' \
306
+ 'user=%<username>s&password=%<password>s&loggerLevel=%<logger_level>s'.freeze
307
+
308
+ # rubocop: disable Metrics/AbcSize
309
+ # rubocop: disable Metrics/CyclomaticComplexity
310
+ # rubocop: disable Metrics/MethodLength
311
+ # rubocop: disable Metrics/PerceivedComplexity
312
+ def switch_database(database_name = database)
313
+ uri = Sequel::Model.db.opts[:uri] || ''
314
+ schema = uri.to_s.scan(/^(\w+(:\w+)?):\/\//).first&.first || 'jdbc:postgresql'
315
+ host = uri.to_s.scan(/:\/\/([\w.\d]+)/).first&.first || localhost
316
+ port = uri.to_s.scan(/:\/\/\w+(:(\d+))?/).first&.first
317
+ username = database_name || uri.to_s.scan(/user=([^&]+)/).first
318
+ password = uri.to_s.scan(/password=([^&]+)/).first&.first || username
319
+ logger_level = 'OFF'
320
+ url = format(DatabaseURITemplate,
321
+ schema: schema,
322
+ host: host,
323
+ port: port.nil? || port.empty? ? '' : ':' + port.to_s,
324
+ database_name: database_name,
325
+ username: username,
326
+ password: password,
327
+ logger_level: logger_level
328
+ )
329
+ reconnect_database(url)
330
+ return true
331
+ end
332
+ # rubocop: enable Metrics/AbcSize
333
+ # rubocop: enable Metrics/CyclomaticComplexity
334
+ # rubocop: enable Metrics/MethodLength
335
+ # rubocop: enable Metrics/PerceivedComplexity
336
+
337
+ # rubocop: disable Metrics/AbcSize
338
+ # rubocop: disable Metrics/CyclomaticComplexity
339
+ # rubocop: disable Metrics/MethodLength
340
+ # rubocop: disable Metrics/PerceivedComplexity
341
+ def switch_user(username = database, database_name = nil)
342
+ uri = Sequel::Model.db.opts[:uri] || ''
343
+ schema = uri.to_s.scan(/^(\w+(:\w+)?):\/\//).first&.first || 'jdbc:postgresql'
344
+ host = uri.to_s.scan(/:\/\/([\w.\d]+)/).first&.first || localhost
345
+ port = uri.to_s.scan(/:\/\/\w+(:(\d+))?/).first&.first
346
+ database_name = database_name || uri.to_s.scan(/\/(\w+)\??(\w+=\w+)*$/).first || database
347
+ password = uri.to_s.scan(/password=([^&]+)/).first&.first || username
348
+ logger_level = 'OFF'
349
+ url = format(DatabaseURITemplate,
350
+ schema: schema,
351
+ host: host,
352
+ port: port.nil? || port.empty? ? '' : ':' + port.to_s,
353
+ database_name: database_name,
354
+ username: username,
355
+ password: password,
356
+ logger_level: logger_level
357
+ )
358
+ reconnect_database(url)
359
+ return true
360
+ end
361
+ # rubocop: enable Metrics/AbcSize
362
+ # rubocop: enable Metrics/CyclomaticComplexity
363
+ # rubocop: enable Metrics/MethodLength
364
+ # rubocop: enable Metrics/PerceivedComplexity
365
+
366
+ # rubocop: disable Metrics/MethodLength
367
+ def create_user(username = database)
368
+ return false if user_exist?(username)
369
+ begin
370
+ log.debug "Creating user: #{username}"
371
+ run "create user #{username}"
372
+ run "alter role #{username} with password '#{username}'"
373
+ return true
374
+ rescue Sequel::DatabaseError => e
375
+ log.error e.inspect
376
+ return false
377
+ rescue StandardError => e
378
+ log.error e.inspect
379
+ return false
380
+ end
381
+ end
382
+ # rubocop: enable Metrics/MethodLength
383
+
384
+ # rubocop: disable Metrics/MethodLength
385
+ def delete_user(username = database)
386
+ return false unless user_exist?(username)
387
+ begin
388
+ log.debug "Dropping user: #{username}"
389
+ run "reassign owned by #{username} to postgres"
390
+ run "drop user #{username}"
391
+ rescue Sequel::DatabaseError => e
392
+ log.error e.inspect
393
+ return false
394
+ rescue StandardError => e
395
+ log.error e.inspect
396
+ return false
397
+ end
398
+ end
399
+ # rubocop: enable Metrics/MethodLength
400
+ end
401
+ # module Utilities
402
+ end
403
+ # module Databases
404
+
405
+ # The Sequel module
406
+ module Sequel
407
+ # The Sequel::Postgres module
408
+ module Postgres
409
+ if defined?(Sequel::Migration)
410
+ # The Sequel::Postgres::Bootstrap class
411
+ class Bootstrap < Sequel::Migration
412
+ include Sequel::Inflections
413
+ include Databases::Helpers
414
+ include Databases::Management
415
+ include Databases::Utilities
416
+
417
+ def up
418
+ create_user
419
+ create_database
420
+ switch_database
421
+ switch_user
422
+ end
423
+
424
+ def down
425
+ switch_database :postgres
426
+ drop_database
427
+ delete_user
428
+ end
429
+
430
+ def self.migrate
431
+ MigrationMethods.each do |method_name|
432
+ Inform::Databases.instances.each_value { |db| db.send(method_name) }
433
+ end
434
+ end
435
+
436
+ def self.up
437
+ Inform::Databases.instances.each_value { |db| db.send(:up) }
438
+ end
439
+ end
440
+ # class Bootstrap
441
+ end
442
+ # defined?(Sequel::Migration)
443
+ end
444
+ # module Postgres
445
+ end
446
+ # module Sequel
447
+
448
+ # The Inform module
449
+ module Inform
450
+ # The Databases module
451
+ module Databases
452
+ @instances = {}
453
+ def instances
454
+ @instances
455
+ end
456
+ module_function :instances
457
+ end
458
+ end
459
+
460
+ if defined?(Sequel::Postgres::Bootstrap)
461
+ # The Inform module
462
+ module Inform
463
+ # The Database abstraction class
464
+ class Database
465
+ attr_reader :bootstrap
466
+
467
+ def self.init(database_name)
468
+ Inform::Database.instance(database_name)
469
+ end
470
+
471
+ @instance_mutex = Mutex.new
472
+
473
+ def self.instance(*args)
474
+ return @instance unless @instance.nil?
475
+ @instance_mutex.synchronize do
476
+ @instance ||= new(*args)
477
+ end
478
+ @instance
479
+ end
480
+
481
+ private_class_method :new
482
+
483
+ def initialize(env = Game.environment)
484
+ log.debug "Initializing persistence layer for environment: #{env}"
485
+ log.debug caller[0..4]
486
+ env ||= environment
487
+ @config = database_config.fetch(env.to_s, {})
488
+ establish_database_connection
489
+ enable_plugins
490
+ end
491
+ REGISTRY = Struct.new(:memo).new(defined?(Java) ? java.util.concurrent.ConcurrentHashMap.new : {})
492
+ def initialize(database_name)
493
+ @bootstrap = Sequel::Postgres::Bootstrap.new(Sequel::Model.db)
494
+ Inform::Databases.instances[database_name] = self
495
+ end
496
+ end
497
+ end
498
+ # module Inform
499
+ end
500
+ # defined?(Sequel::Postgres::Bootstrap)