story-teller 1.1.3
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 +7 -0
- data/LICENSE +623 -0
- data/README.md +188 -0
- data/Rakefile +58 -0
- data/config/database.yml +37 -0
- data/exe/inform.rb +6 -0
- data/game/config.yml +5 -0
- data/game/example.inf +90 -0
- data/game/example.rb +105 -0
- data/game/forms/example_form.rb +2 -0
- data/game/grammar/admin.inf.rb +185 -0
- data/game/grammar/builder.inf.rb +310 -0
- data/game/grammar/game_grammar.inf.rb +6 -0
- data/game/grammar/meta.inf.rb +41 -0
- data/game/languages/english.rb +571 -0
- data/game/models/example_model.rb +2 -0
- data/game/modules/example_module.rb +9 -0
- data/game/modules/parser_extensions.rb +264 -0
- data/game/rules/example_state.rb +2 -0
- data/game/scripts/example_script.rb +2 -0
- data/game/topics/example_topic.rb +2 -0
- data/game/verbs/game_verbs.rb +35 -0
- data/game/verbs/metaverbs.rb +2066 -0
- data/lib/story_teller/application.rb +82 -0
- data/lib/story_teller/cli.rb +35 -0
- data/lib/story_teller/color.rb +144 -0
- data/lib/story_teller/config.rb +61 -0
- data/lib/story_teller/curses_adapter.rb +30 -0
- data/lib/story_teller/database.rb +527 -0
- data/lib/story_teller/game/loader.rb +276 -0
- data/lib/story_teller/game.rb +22 -0
- data/lib/story_teller/inform/models.rb +42 -0
- data/lib/story_teller/inform/relational/link.rb +239 -0
- data/lib/story_teller/inform/relational/module.rb +203 -0
- data/lib/story_teller/inform/relational/object.rb +546 -0
- data/lib/story_teller/inform/relational/tag.rb +152 -0
- data/lib/story_teller/options.rb +151 -0
- data/lib/story_teller/persistence.rb +340 -0
- data/lib/story_teller/player_character.rb +99 -0
- data/lib/story_teller/privileges.rb +55 -0
- data/lib/story_teller/runtime.rb +381 -0
- data/lib/story_teller/snapshots.rb +412 -0
- data/lib/story_teller/terminal.rb +58 -0
- data/lib/story_teller/version.rb +24 -0
- data/lib/story_teller_cli.rb +34 -0
- metadata +158 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# lib/story_teller/game/loader.rb
|
|
2
|
+
# encoding: utf-8
|
|
3
|
+
# frozen_string_literal: false
|
|
4
|
+
|
|
5
|
+
# Copyright Nels Nelson 2008-2025 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
|
+
require 'yaml'
|
|
23
|
+
|
|
24
|
+
# The StoryTeller module
|
|
25
|
+
module StoryTeller
|
|
26
|
+
# The Game class
|
|
27
|
+
class Game
|
|
28
|
+
attr_reader :config, :config_file_path, :file_path, :path
|
|
29
|
+
|
|
30
|
+
def initialize(options)
|
|
31
|
+
@config = {}
|
|
32
|
+
@path, @file_path = game_paths(options)
|
|
33
|
+
@config_file_path = File.join(@path, options[:game_config_file_name])
|
|
34
|
+
@config = YAML.load_file(@config_file_path, symbolize_names: true)
|
|
35
|
+
@config[:game_file_path] = @file_path unless @file_path.nil?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def name
|
|
39
|
+
@config[:game_name]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def environment
|
|
43
|
+
(
|
|
44
|
+
ENV['GAME_ENVIRONMENT'] ||
|
|
45
|
+
@config[:environment] ||
|
|
46
|
+
StoryTeller::Engine.default_environment
|
|
47
|
+
).to_sym
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def environment=(env)
|
|
51
|
+
@config[:environment] = env.to_sym
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def game_paths(options)
|
|
57
|
+
path = File.expand_path(options[:game_path])
|
|
58
|
+
path = StoryTeller::Config.default_game_dir_path unless game_config_exists?(path, options)
|
|
59
|
+
return [path, nil] if File.directory?(path)
|
|
60
|
+
|
|
61
|
+
[File.dirname(path), path]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def game_config_exists?(path, options)
|
|
65
|
+
config_dir = File.directory?(path) ? path : File.dirname(path)
|
|
66
|
+
File.exist?(File.join(config_dir, options[:game_config_file_name]))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def game_dir_path(path)
|
|
70
|
+
return path if File.directory?(path)
|
|
71
|
+
@config[:game_file_path] = path
|
|
72
|
+
@game_dir_path = File.expand_path(File.dirname(path))
|
|
73
|
+
return @game_dir_path if File.exist?(@config_file_path)
|
|
74
|
+
StoryTeller::Config.default_game_dir_path
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
class GameLoadError < StandardError; end
|
|
78
|
+
|
|
79
|
+
# The Game::Loader module loads a game from a given directory.
|
|
80
|
+
module Loader
|
|
81
|
+
PreGameComponents = %i[modules].freeze
|
|
82
|
+
|
|
83
|
+
def load_game(game_dir_path = game_path)
|
|
84
|
+
self.load_path = game_dir_path
|
|
85
|
+
load_game_subcomponents(pre_game_components)
|
|
86
|
+
load_game_files(top_level_game_path(game_dir_path))
|
|
87
|
+
load_library
|
|
88
|
+
load_game_subcomponents(post_game_components)
|
|
89
|
+
load_grammars
|
|
90
|
+
reset_constants
|
|
91
|
+
rescue StandardError => e
|
|
92
|
+
handle_load_failure(e)
|
|
93
|
+
raise
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def top_level_game_path(game_dir_path)
|
|
97
|
+
return game_dir_path unless same_path?(game_dir_path, game.path)
|
|
98
|
+
|
|
99
|
+
game.file_path || game_dir_path
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def same_path?(first, second)
|
|
103
|
+
File.expand_path(first) == File.expand_path(second)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def first_orphan_object_with_description_that_has_light
|
|
107
|
+
Inform::Object.all.find { |o| !o.description.nil? && o.has?(:light) }&.name
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def load_path=(path)
|
|
111
|
+
@game_dir_path = path
|
|
112
|
+
$LOAD_PATH.unshift(path) unless $LOAD_PATH.include?(path)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def load_game_subcomponents(components = game_components)
|
|
116
|
+
components.each { |component| load_game_sub(component) }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def pre_game_components
|
|
120
|
+
game_components & PreGameComponents
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def post_game_components
|
|
124
|
+
game_components - pre_game_components
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def reset_constants
|
|
128
|
+
reset_constant(:HDR_GAMESERIAL, game_serial)
|
|
129
|
+
reset_constant(:HDR_GAMERELEASE, release)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def game_serial
|
|
133
|
+
defined?(::Serial) ? ::Serial : File.mtime(@game_dir_path).strftime('%y%m%d')
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def release
|
|
137
|
+
defined?(::Release) ? ::Release : 0
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def load_game_sub(component, start = Time.now)
|
|
141
|
+
game_component_path = File.join(@game_dir_path, component.to_s)
|
|
142
|
+
Dir.glob(File.join(game_component_path, '*')).each do |file|
|
|
143
|
+
next unless File.file?(file)
|
|
144
|
+
log.debug "Loading file: #{file}"
|
|
145
|
+
require file
|
|
146
|
+
end
|
|
147
|
+
ensure
|
|
148
|
+
elapsed = (Time.now - start) * 1000
|
|
149
|
+
log.debug format("Loaded #{component} in %0.2f milliseconds", elapsed)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# def load_game_files(game_path = @game_dir_path)
|
|
153
|
+
# log.info "Loading story game #{game.name} from #{game_path}"
|
|
154
|
+
# Dir.glob(File.join(game_path, '*.rb')).each do |file|
|
|
155
|
+
# next unless File.file?(file)
|
|
156
|
+
# log.trace "Loading file: #{file}"
|
|
157
|
+
# require file
|
|
158
|
+
# end
|
|
159
|
+
# log.debug "Loaded story game #{game.name}"
|
|
160
|
+
# end
|
|
161
|
+
|
|
162
|
+
def load_game_files(game_path = @game_dir_path)
|
|
163
|
+
log.info "Loading story game #{game.name} from #{game_path}"
|
|
164
|
+
|
|
165
|
+
if preserve_persisted_world?
|
|
166
|
+
load_game_files_without_committing(game_path)
|
|
167
|
+
else
|
|
168
|
+
load_game_files_with_committing(game_path)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
log.debug "Loaded story game #{game.name}"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def preserve_persisted_world?
|
|
175
|
+
options[:persist] && persistence.world_tree?
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def load_game_files_with_committing(game_path)
|
|
179
|
+
game_files(game_path).each do |file|
|
|
180
|
+
log.trace "Loading file: #{file}"
|
|
181
|
+
require file
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def game_files(game_path)
|
|
186
|
+
return [game_path] if File.file?(game_path)
|
|
187
|
+
|
|
188
|
+
Dir.glob(File.join(game_path, '*.rb')).select { |file| File.file?(file) }
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def load_game_files_without_committing(game_path)
|
|
192
|
+
preserved_ids = persistence.world_object_ids
|
|
193
|
+
|
|
194
|
+
persistence.without_committing! do
|
|
195
|
+
load_game_files_with_committing(game_path)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
refresh_preserved_world_objects(preserved_ids)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def refresh_preserved_world_objects(preserved_ids)
|
|
202
|
+
ObjectSpace.each_object(Inform::Object) do |object|
|
|
203
|
+
refresh_preserved_world_object(object, preserved_ids)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def refresh_preserved_world_object(object, preserved_ids)
|
|
208
|
+
return if object.id.nil?
|
|
209
|
+
|
|
210
|
+
unless preserved_ids.include?(object.id)
|
|
211
|
+
raise GameLoadError, "Source object is missing from preserved world: #{object.object_name}"
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
object.associations.clear if object.respond_to?(:associations)
|
|
215
|
+
object.refresh
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def handle_load_failure(e)
|
|
219
|
+
case e.message
|
|
220
|
+
when /PG::UndefinedTable/
|
|
221
|
+
log.error "Fatal: Database initialization is required"
|
|
222
|
+
abort
|
|
223
|
+
else
|
|
224
|
+
log.error "Error loading game: #{e.class.name}: #{e.message}"
|
|
225
|
+
e.backtrace.each { |t| log.error t }
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Move to the front of the list any element matching the given condition
|
|
230
|
+
def prioritize_if(list, &condition)
|
|
231
|
+
element = list.find(&condition)
|
|
232
|
+
return list if element.nil?
|
|
233
|
+
list.unshift(list.delete(element))
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
EnsureFirst = [/emotes.inf/, /grammar.inf/].freeze
|
|
237
|
+
|
|
238
|
+
def grammar_files
|
|
239
|
+
files = Dir.glob(File.join(grammar_module_path, '*'))
|
|
240
|
+
EnsureFirst.each do |pattern|
|
|
241
|
+
prioritize_if(files) { |file_path| file_path.match?(pattern) }
|
|
242
|
+
end
|
|
243
|
+
files
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
GrammarParsedInfoMessage = 'Parsed grammar %<file>s in %0.2<elapsed>f milliseconds'.freeze
|
|
247
|
+
|
|
248
|
+
def load_grammars
|
|
249
|
+
grammar_files.each do |file_path|
|
|
250
|
+
start = Time.now
|
|
251
|
+
StoryTeller::Library::Loader.load_grammar_by_path(file_path)
|
|
252
|
+
elapsed = (Time.now - start) * 1000
|
|
253
|
+
log.debug format(GrammarParsedInfoMessage, file: File.basename(file_path), elapsed: elapsed)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def load_library
|
|
258
|
+
StoryTeller::Library::Loader.load_library
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def load_game_states
|
|
262
|
+
states = game.config.fetch(:additional_promiscuous_states, []).map(&:to_sym)
|
|
263
|
+
Session.add_promiscuous_states(states)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def reload_game
|
|
267
|
+
load_game
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
# module Loader
|
|
271
|
+
|
|
272
|
+
StoryTeller::Runtime.include(StoryTeller::Game::Loader)
|
|
273
|
+
end
|
|
274
|
+
# class Game
|
|
275
|
+
end
|
|
276
|
+
# module StoryTeller
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# lib/story_teller/game.rb
|
|
2
|
+
# encoding: utf-8
|
|
3
|
+
# frozen_string_literal: false
|
|
4
|
+
|
|
5
|
+
# Copyright Nels Nelson 2008-2025 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
|
+
require_relative 'game/loader'
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
# frozen_string_literal: false
|
|
3
|
+
|
|
4
|
+
# Copyright Nels Nelson 2008-2026 but freely usable (see license)
|
|
5
|
+
#
|
|
6
|
+
# This file is part of StoryTeller.
|
|
7
|
+
#
|
|
8
|
+
# The StoryTeller 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 StoryTeller 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 StoryTeller. If not, see <http://www.gnu.org/licenses/>.
|
|
20
|
+
|
|
21
|
+
require_relative 'relational/object'
|
|
22
|
+
require_relative 'relational/link'
|
|
23
|
+
require_relative 'relational/module'
|
|
24
|
+
require_relative 'relational/tag'
|
|
25
|
+
|
|
26
|
+
# module Inform
|
|
27
|
+
module Inform
|
|
28
|
+
# class Object
|
|
29
|
+
class Object
|
|
30
|
+
include StoryTeller::Builtins
|
|
31
|
+
include StoryTeller::IO if defined?(StoryTeller::IO)
|
|
32
|
+
include StoryTeller::Publisher if defined?(StoryTeller::Publisher)
|
|
33
|
+
include StoryTeller::Daemons if defined?(StoryTeller::Daemons)
|
|
34
|
+
include Inform::Context if defined?(Inform::Context)
|
|
35
|
+
include Inform::Events if defined?(Inform::Events)
|
|
36
|
+
include Inform::Prototypical if defined?(Inform::Prototypical)
|
|
37
|
+
prepend Inform::Linkable if defined?(Inform::Linkable)
|
|
38
|
+
include Inform::Taggable if defined?(Inform::Taggable)
|
|
39
|
+
include Inform::Modular if defined?(Inform::Modular)
|
|
40
|
+
include Inform::Genealogical if defined?(Inform::Genealogical)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# lib/story_teller/inform/relational/link.rb
|
|
2
|
+
# encoding: utf-8
|
|
3
|
+
# frozen_string_literal: false
|
|
4
|
+
|
|
5
|
+
# Copyright Nels Nelson 2008-2025 but freely usable (see license)
|
|
6
|
+
#
|
|
7
|
+
# This file is part of the Inform Runtime.
|
|
8
|
+
#
|
|
9
|
+
# The Inform Runtime 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 Inform Runtime 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 Inform Runtime. If not, see <http://www.gnu.org/licenses/>.
|
|
21
|
+
|
|
22
|
+
# Link
|
|
23
|
+
|
|
24
|
+
if defined?(Sequel::Migration)
|
|
25
|
+
# The LinkSetup class
|
|
26
|
+
class LinkSetup < Sequel::Migration
|
|
27
|
+
# rubocop: disable Metrics/MethodLength
|
|
28
|
+
def up
|
|
29
|
+
# return if table_exists? :link
|
|
30
|
+
log.debug "#up"
|
|
31
|
+
create_table? :link do
|
|
32
|
+
primary_key :id
|
|
33
|
+
foreign_key :from_id, :object, on_delete: :cascade
|
|
34
|
+
foreign_key :to_id, :object, on_delete: :cascade
|
|
35
|
+
index :name
|
|
36
|
+
index :created_at
|
|
37
|
+
|
|
38
|
+
String :name
|
|
39
|
+
DateTime :created_at
|
|
40
|
+
DateTime :modified_at
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
# rubocop: enable Metrics/MethodLength
|
|
44
|
+
|
|
45
|
+
def down
|
|
46
|
+
drop_table :link if table_exists? :link
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
# class LinkSetup
|
|
50
|
+
end
|
|
51
|
+
# defined?(Sequel::Migration)
|
|
52
|
+
|
|
53
|
+
# module Inform
|
|
54
|
+
module Inform
|
|
55
|
+
# class Link
|
|
56
|
+
class Link < Sequel::Model
|
|
57
|
+
set_primary_key :id
|
|
58
|
+
def_column_accessor :created_at, :modified_at
|
|
59
|
+
def_column_accessor :name
|
|
60
|
+
many_to_one :from, class: Inform::Object, key: :from_id
|
|
61
|
+
many_to_one :to, class: Inform::Object, key: :to_id
|
|
62
|
+
|
|
63
|
+
LinkTemplate = '%<link_name>s -> %<to_name>s [%<to_identity>s]'.freeze
|
|
64
|
+
LinkMethods = %w[from_id to_id].freeze
|
|
65
|
+
MethodWriterTemplate = '%<method_name>s='.freeze
|
|
66
|
+
|
|
67
|
+
def initialize(values = {}, &block)
|
|
68
|
+
super(self.class.relational_values(values), &block)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.relational_values(values)
|
|
72
|
+
return values unless values.is_a?(Hash)
|
|
73
|
+
|
|
74
|
+
values = values.dup
|
|
75
|
+
from = values.delete(:from)
|
|
76
|
+
to = values.delete(:to)
|
|
77
|
+
values[:from_id] ||= persisted_object_id(from)
|
|
78
|
+
values[:to_id] ||= persisted_object_id(to)
|
|
79
|
+
values
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# rubocop: disable Metrics/CyclomaticComplexity
|
|
83
|
+
def self.persisted_object_id(object)
|
|
84
|
+
return nil if object.nil?
|
|
85
|
+
return object.id if object.respond_to?(:id) && object.id
|
|
86
|
+
object.save_changes if object.respond_to?(:save_changes)
|
|
87
|
+
return object.id if object.respond_to?(:id) && object.id
|
|
88
|
+
return object.pk if object.respond_to?(:pk)
|
|
89
|
+
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
# rubocop: enable Metrics/CyclomaticComplexity
|
|
93
|
+
|
|
94
|
+
def before_create
|
|
95
|
+
self.created_at ||= Time.now
|
|
96
|
+
super
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def after_save
|
|
100
|
+
super
|
|
101
|
+
self.modified_at = Time.now.utc
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def to_s
|
|
105
|
+
format(LinkTemplate, link_name: name, to_name: to&.name, to_identity: to&.identity)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def <=>(other)
|
|
109
|
+
self.name <=> other.name
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def init_with(coder)
|
|
113
|
+
LinkMethods.each do |method_name|
|
|
114
|
+
method_symbol = format(MethodWriterTemplate, method_name: method_name).to_sym
|
|
115
|
+
self.send(method_symbol, coder[method_name]) if self.respond_to? method_symbol
|
|
116
|
+
end
|
|
117
|
+
self
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
# module Inform
|
|
122
|
+
|
|
123
|
+
# module Inform
|
|
124
|
+
module Inform
|
|
125
|
+
module_function
|
|
126
|
+
|
|
127
|
+
def link_klass
|
|
128
|
+
Inform::Link
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# module Inform
|
|
133
|
+
module Inform
|
|
134
|
+
# module Linkable
|
|
135
|
+
module Linkable
|
|
136
|
+
def links
|
|
137
|
+
return Array::Empty unless self.respond_to?(:id)
|
|
138
|
+
return Array::Empty if self.id.nil?
|
|
139
|
+
|
|
140
|
+
Inform.link_klass.filter(from_id: self.id).all
|
|
141
|
+
rescue StandardError => _e
|
|
142
|
+
Array::Empty
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def find_link(link_name)
|
|
146
|
+
return nil unless self.respond_to?(:id)
|
|
147
|
+
return nil if self.id.nil?
|
|
148
|
+
|
|
149
|
+
Inform.link_klass.first(name: link_name.to_s, from_id: self.id)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# rubocop: disable Metrics/MethodLength
|
|
153
|
+
def link(link_name, obj = nil)
|
|
154
|
+
save_link_endpoint(self)
|
|
155
|
+
save_link_endpoint(obj)
|
|
156
|
+
|
|
157
|
+
link = find_link(link_name)
|
|
158
|
+
return link if obj.nil?
|
|
159
|
+
|
|
160
|
+
if link.nil?
|
|
161
|
+
clauses = { name: link_name.to_s, from_id: self.id, to_id: obj.id }
|
|
162
|
+
link = Inform.link_klass.find_or_create(**clauses)
|
|
163
|
+
else
|
|
164
|
+
link.to_id = obj.id
|
|
165
|
+
link.save
|
|
166
|
+
end
|
|
167
|
+
link
|
|
168
|
+
end
|
|
169
|
+
# rubocop: enable Metrics/MethodLength
|
|
170
|
+
|
|
171
|
+
def linked?(link_name)
|
|
172
|
+
return false unless self.respond_to?(:id)
|
|
173
|
+
return false if self.id.nil?
|
|
174
|
+
|
|
175
|
+
Inform.link_klass.filter(name: link_name.to_s, from_id: self.id).count > 0
|
|
176
|
+
end
|
|
177
|
+
alias _key? linked?
|
|
178
|
+
|
|
179
|
+
def unlink(link_name)
|
|
180
|
+
link = find_link(link_name)
|
|
181
|
+
return nil if link.nil?
|
|
182
|
+
|
|
183
|
+
object = linked_object(link.to_id)
|
|
184
|
+
link.destroy
|
|
185
|
+
object
|
|
186
|
+
rescue Sequel::NoExistingObject => e
|
|
187
|
+
log.warn 'Error: ' + e.message
|
|
188
|
+
log.warn 'No such link: ' + link_name.to_s
|
|
189
|
+
nil
|
|
190
|
+
end
|
|
191
|
+
alias _unset_object unlink
|
|
192
|
+
|
|
193
|
+
def linkto(link_name)
|
|
194
|
+
link = find_link(link_name)
|
|
195
|
+
link.nil? ? nil : linked_object(link.to_id)
|
|
196
|
+
end
|
|
197
|
+
alias _get_object linkto
|
|
198
|
+
|
|
199
|
+
def _set_object(link_name, obj = nil)
|
|
200
|
+
return nil if obj.nil?
|
|
201
|
+
|
|
202
|
+
link(link_name, obj)
|
|
203
|
+
obj
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def linksfrom(link_name = nil)
|
|
207
|
+
return Array::Empty unless self.respond_to?(:id)
|
|
208
|
+
return Array::Empty if self.id.nil?
|
|
209
|
+
|
|
210
|
+
dataset = Inform.link_klass.filter(to_id: self.id)
|
|
211
|
+
dataset = dataset.filter(name: link_name.to_s) unless link_name.nil?
|
|
212
|
+
dataset.all.map { |link| linked_object(link.from_id) }.compact
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
private
|
|
216
|
+
|
|
217
|
+
def save_link_endpoint(object)
|
|
218
|
+
return if object.nil?
|
|
219
|
+
return if object.respond_to?(:id) && object.id
|
|
220
|
+
|
|
221
|
+
if object.respond_to?(:save_changes)
|
|
222
|
+
object.save_changes
|
|
223
|
+
elsif object.respond_to?(:save)
|
|
224
|
+
object.save
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def linked_object(id)
|
|
229
|
+
return nil if id.nil?
|
|
230
|
+
return Inform::Object[id] if defined?(Inform::Object) && Inform::Object.respond_to?(:[])
|
|
231
|
+
return Inform::Ephemeral::Object[id] if defined?(Inform::Ephemeral::Object) &&
|
|
232
|
+
Inform::Ephemeral::Object.respond_to?(:[])
|
|
233
|
+
|
|
234
|
+
nil
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
# module Linkable
|
|
238
|
+
end
|
|
239
|
+
# module Inform
|