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,546 @@
|
|
|
1
|
+
# lib/story_teller/inform/relational/object.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
|
+
require 'sequel'
|
|
23
|
+
|
|
24
|
+
# Object
|
|
25
|
+
|
|
26
|
+
if defined? Sequel::Migration
|
|
27
|
+
# The ObjectSetup class
|
|
28
|
+
class ObjectSetup < Sequel::Migration
|
|
29
|
+
# rubocop: disable Metrics/MethodLength
|
|
30
|
+
def up
|
|
31
|
+
create_table? :object do
|
|
32
|
+
primary_key :id
|
|
33
|
+
foreign_key :parent_id, :object, on_delete: :set_null
|
|
34
|
+
index :name
|
|
35
|
+
index :created_at
|
|
36
|
+
|
|
37
|
+
String :name, text: true
|
|
38
|
+
String :short_name, text: true
|
|
39
|
+
String :description, text: true
|
|
40
|
+
String :object_type, null: false
|
|
41
|
+
String :properties, text: true, default: {}.to_yaml.strip
|
|
42
|
+
DateTime :created_at
|
|
43
|
+
DateTime :modified_at
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
# rubocop: enable Metrics/MethodLength
|
|
47
|
+
|
|
48
|
+
def down
|
|
49
|
+
drop_table(:object, cascade: true) if table_exists? :object
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# if defined? ObjectSetup
|
|
55
|
+
# ObjectSetup.down if ObjectSetup.respond_to?(:down)
|
|
56
|
+
# end
|
|
57
|
+
if defined? ObjectSetup
|
|
58
|
+
ObjectSetup.up if ObjectSetup.respond_to?(:up)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def Object(name, klass = Inform::Object, &block)
|
|
62
|
+
obj = if klass.respond_to?(:fetch_or_create_by_name)
|
|
63
|
+
klass.fetch_or_create_by_name(name)
|
|
64
|
+
else
|
|
65
|
+
klass.new(name)
|
|
66
|
+
end
|
|
67
|
+
obj.with(&block) if block_given?
|
|
68
|
+
obj.save_changes if obj.respond_to?(:save_changes)
|
|
69
|
+
obj
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# The Inform module
|
|
73
|
+
module Inform
|
|
74
|
+
# The Inform::Object class
|
|
75
|
+
# rubocop: disable Metrics/ClassLength
|
|
76
|
+
class Object < Sequel::Model
|
|
77
|
+
logger
|
|
78
|
+
plugin :rcte_tree
|
|
79
|
+
plugin :serialization
|
|
80
|
+
plugin :single_table_inheritance, :object_type
|
|
81
|
+
serialize_attributes :yaml, :properties
|
|
82
|
+
|
|
83
|
+
one_to_many :links, join_table: :link, key: :from_id
|
|
84
|
+
one_to_many :tagged, key: :object_id
|
|
85
|
+
one_to_many :modularized, key: :object_id
|
|
86
|
+
many_to_many :modules,
|
|
87
|
+
class: 'Inform::Module',
|
|
88
|
+
join_table: :modularized,
|
|
89
|
+
left_key: :object_id,
|
|
90
|
+
right_key: :module_id
|
|
91
|
+
many_to_many :tags,
|
|
92
|
+
class: 'Inform::Tag',
|
|
93
|
+
join_table: :tagged,
|
|
94
|
+
left_key: :object_id,
|
|
95
|
+
right_key: :tag_id
|
|
96
|
+
one_to_many :children,
|
|
97
|
+
key: :parent_id,
|
|
98
|
+
class: self,
|
|
99
|
+
order: %i[name id],
|
|
100
|
+
eager: %i[modules tags links]
|
|
101
|
+
|
|
102
|
+
# rubocop: disable Metrics/MethodLength
|
|
103
|
+
def initialize(*args, &block)
|
|
104
|
+
if args.empty?
|
|
105
|
+
super(&nil)
|
|
106
|
+
elsif args.first.is_a?(String)
|
|
107
|
+
short_name = args.first
|
|
108
|
+
super({ name: short_name, short_name: short_name }, &nil)
|
|
109
|
+
else
|
|
110
|
+
super(*args, &nil)
|
|
111
|
+
end
|
|
112
|
+
save
|
|
113
|
+
@tags_semaphore = Mutex.new
|
|
114
|
+
@semaphore = Mutex.new
|
|
115
|
+
self.with(&block) unless block.nil?
|
|
116
|
+
log.debug "Initializing #{self}"
|
|
117
|
+
end
|
|
118
|
+
# rubocop: enable Metrics/MethodLength
|
|
119
|
+
|
|
120
|
+
def self.fetch_or_create_by_name(name)
|
|
121
|
+
key = name.to_s
|
|
122
|
+
object = first(name: key)
|
|
123
|
+
object ||= all.find { |candidate| candidate.name_values.include?(key) }
|
|
124
|
+
object || new(key)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def name_values
|
|
128
|
+
Array(self.values[:name]).flatten.map(&:to_s)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# def name(*args)
|
|
132
|
+
# return args.first.object_name if args.first.respond_to?(:object_name)
|
|
133
|
+
# return self.values[:name] if args.empty?
|
|
134
|
+
|
|
135
|
+
# self.values[:name] = args.length > 1 ? args.join(' ') : args
|
|
136
|
+
# self.save
|
|
137
|
+
# self.values[:name]
|
|
138
|
+
# end
|
|
139
|
+
def name(*args)
|
|
140
|
+
return args.first.object_name if args.first.respond_to?(:object_name)
|
|
141
|
+
return self.values[:name] if args.empty?
|
|
142
|
+
|
|
143
|
+
names = args.flatten
|
|
144
|
+
self.values[:name] = names
|
|
145
|
+
self.save
|
|
146
|
+
self.values[:name] = names
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def name=(value)
|
|
150
|
+
name(value)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# def name_words
|
|
154
|
+
# self.values[:name].split(/[,\s]+/).join(', ')
|
|
155
|
+
# end
|
|
156
|
+
def name_words
|
|
157
|
+
name_values.join(', ')
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# rubocop: disable Metrics/MethodLength
|
|
161
|
+
def short_name(*args)
|
|
162
|
+
unless args.empty?
|
|
163
|
+
self.short_name = args.first
|
|
164
|
+
return
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
textual_name = self.values[:short_name]
|
|
168
|
+
synonyms = self.values[:name]
|
|
169
|
+
if textual_name.nil? || (textual_name.respond_to?(:empty?) && textual_name.empty?)
|
|
170
|
+
self.values[:short_name] = synonyms
|
|
171
|
+
self.save
|
|
172
|
+
self.values[:short_name]
|
|
173
|
+
else
|
|
174
|
+
textual_name
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
# rubocop: enable Metrics/MethodLength
|
|
178
|
+
|
|
179
|
+
def object_name
|
|
180
|
+
self.values[:display_name] || self.values[:short_name] || self.values[:name]
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# def description(*args)
|
|
184
|
+
# return self.values[:description] if args.empty?
|
|
185
|
+
|
|
186
|
+
# value = args.length > 1 ? args.join : args.first
|
|
187
|
+
# self.values[:description] = value
|
|
188
|
+
# self.save_changes if self.respond_to?(:save_changes)
|
|
189
|
+
# value
|
|
190
|
+
# end
|
|
191
|
+
|
|
192
|
+
# alias description= description
|
|
193
|
+
|
|
194
|
+
def description(*args)
|
|
195
|
+
return self[:description] if args.empty?
|
|
196
|
+
|
|
197
|
+
value = args.length > 1 ? args.join : args.first
|
|
198
|
+
self[:description] = value
|
|
199
|
+
save_changes if respond_to?(:save_changes)
|
|
200
|
+
value
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def description=(value)
|
|
204
|
+
self[:description] = value
|
|
205
|
+
save_changes if respond_to?(:save_changes)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def semaphore
|
|
209
|
+
@semaphore ||= Mutex.new
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def tags_semaphore
|
|
213
|
+
@tags_semaphore ||= Mutex.new
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
alias tags_original tags
|
|
217
|
+
|
|
218
|
+
def tags
|
|
219
|
+
tags_semaphore.synchronize { tags_original }
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def routines
|
|
223
|
+
((self.methods - Object.instance_methods) - Inform::Object.instance_methods).sort.uniq
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def <=>(other)
|
|
227
|
+
self.name <=> other.name
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def ==(other)
|
|
231
|
+
other.respond_to?(:values) ? self.values[:id] == other.values[:id] : super
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def parse(s)
|
|
235
|
+
return if inflib.nil?
|
|
236
|
+
|
|
237
|
+
semaphore.synchronize do
|
|
238
|
+
inflib.parse s
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def invoke(a, *args)
|
|
243
|
+
return if inflib.nil?
|
|
244
|
+
|
|
245
|
+
inflib.invoke(a, *args)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def _invoke(a, *args)
|
|
249
|
+
return if inflib.nil?
|
|
250
|
+
|
|
251
|
+
inflib._invoke(a, *args)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def __invoke(a, *args)
|
|
255
|
+
return if inflib.nil?
|
|
256
|
+
|
|
257
|
+
inflib.__invoke(a, *args)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def inflib
|
|
261
|
+
return InformLibrary[self] if self.has?(:animate) ||
|
|
262
|
+
defined?(Character) && self.is_a?(Character)
|
|
263
|
+
|
|
264
|
+
@inflib
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# rubocop: disable Style/TrivialAccessors
|
|
268
|
+
def inflib=(inflib)
|
|
269
|
+
@inflib = inflib
|
|
270
|
+
end
|
|
271
|
+
# rubocop: enable Style/TrivialAccessors
|
|
272
|
+
|
|
273
|
+
def print_zmachine_result(s, should_prompt: true)
|
|
274
|
+
inflib&.print_zmachine_result(s, should_prompt:)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def print_evented_zmachine_result(s, isolate: false)
|
|
278
|
+
inflib&.print_evented_zmachine_result(s, isolate: isolate)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
Visited = defined?(ConcurrentHashMap) ? ConcurrentHashMap.new : {}
|
|
282
|
+
|
|
283
|
+
def visited
|
|
284
|
+
Visited[self] ||= []
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def after_create
|
|
288
|
+
super
|
|
289
|
+
index_words if respond_to?(:index_words)
|
|
290
|
+
init
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def init
|
|
294
|
+
# A designer should implement this in a subclass if necessary
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def safe_refresh
|
|
298
|
+
self.refresh
|
|
299
|
+
rescue Sequel::NoExistingObject => e
|
|
300
|
+
log.warn "Object refresh failure: #{e.message}; ignoring"
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def safe_save
|
|
304
|
+
self.save
|
|
305
|
+
rescue Sequel::NoExistingObject => e
|
|
306
|
+
log.warn "Object save failure: #{e.message}; ignoring"
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def safe_init
|
|
310
|
+
self.init if self.respond_to? :init
|
|
311
|
+
rescue Sequel::NoExistingObject => e
|
|
312
|
+
log.warn "Object initialization failure: #{e.message}; ignoring"
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def classify_as(klass)
|
|
316
|
+
return if klass.nil?
|
|
317
|
+
|
|
318
|
+
klass = find_class klass unless klass.is_a? Class
|
|
319
|
+
self.object_type = klass.name
|
|
320
|
+
self.safe_save
|
|
321
|
+
self.safe_init
|
|
322
|
+
o = Inform::Object[self.id]
|
|
323
|
+
log.debug "o.object_type: #{o.object_type}"
|
|
324
|
+
return o
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# def ephemeral_children
|
|
328
|
+
# EphemeralObjectsChildren[identity] ||= []
|
|
329
|
+
# end
|
|
330
|
+
|
|
331
|
+
# # TODO: Implement a unit test for this
|
|
332
|
+
# def <<(o)
|
|
333
|
+
# return if o.nil?
|
|
334
|
+
# return if o == self
|
|
335
|
+
|
|
336
|
+
# if o.ephemeral?
|
|
337
|
+
# o.parent = self
|
|
338
|
+
# ephemeral_children << o
|
|
339
|
+
# else
|
|
340
|
+
# self.add_child o
|
|
341
|
+
# self.save
|
|
342
|
+
# self.refresh
|
|
343
|
+
# end
|
|
344
|
+
# end
|
|
345
|
+
|
|
346
|
+
def ephemeral_children
|
|
347
|
+
EphemeralObjectsChildren[identity] ||= []
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# rubocop: disable Metrics/AbcSize
|
|
351
|
+
# rubocop: disable Metrics/CyclomaticComplexity
|
|
352
|
+
# rubocop: disable Metrics/MethodLength
|
|
353
|
+
# rubocop: disable Metrics/PerceivedComplexity
|
|
354
|
+
# TODO: Implement a unit test for this
|
|
355
|
+
def <<(o)
|
|
356
|
+
return if o.nil?
|
|
357
|
+
return if o == self
|
|
358
|
+
|
|
359
|
+
if o.ephemeral?
|
|
360
|
+
o.parent = self
|
|
361
|
+
ephemeral_children << o
|
|
362
|
+
else
|
|
363
|
+
self.add_child o if self.respond_to?(:add_child)
|
|
364
|
+
o.parent_id = self.id if o.respond_to?(:parent_id=)
|
|
365
|
+
o.save_changes if o.respond_to?(:save_changes)
|
|
366
|
+
self.save
|
|
367
|
+
self.associations.delete(:children) if self.respond_to?(:associations)
|
|
368
|
+
self.refresh
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
# rubocop: enable Metrics/AbcSize
|
|
372
|
+
# rubocop: enable Metrics/CyclomaticComplexity
|
|
373
|
+
# rubocop: enable Metrics/MethodLength
|
|
374
|
+
# rubocop: enable Metrics/PerceivedComplexity
|
|
375
|
+
|
|
376
|
+
def remove
|
|
377
|
+
self.parent.remove_child(self.id) if self.parent&.children&.include?(self)
|
|
378
|
+
rescue StandardError => e
|
|
379
|
+
log.error e
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def empty?
|
|
383
|
+
return false if ephemeral_children.any?
|
|
384
|
+
return children_dataset.count.zero? if self.respond_to?(:children_dataset)
|
|
385
|
+
|
|
386
|
+
children.empty?
|
|
387
|
+
rescue StandardError => e
|
|
388
|
+
log.error "Unexpected error getting children count for #{self.class} #{self.id}: #{e.message}", $ERROR_INFO
|
|
389
|
+
children.empty?
|
|
390
|
+
end
|
|
391
|
+
# def empty?
|
|
392
|
+
# begin
|
|
393
|
+
# n = db.fetch("select count(*) from object where parent_id = #{self.id}").first[:count].to_i
|
|
394
|
+
# rescue StandardError => e
|
|
395
|
+
# log.error "Unexpected error getting children count for #{self.class} #{self.id}: #{e.message}", $ERROR_INFO
|
|
396
|
+
# n = children.length
|
|
397
|
+
# end
|
|
398
|
+
# n == 0
|
|
399
|
+
# end
|
|
400
|
+
|
|
401
|
+
def child(o = nil)
|
|
402
|
+
return o.child unless o.nil?
|
|
403
|
+
return self.children_dataset.first if self.respond_to?(:children_dataset)
|
|
404
|
+
|
|
405
|
+
self.children.first
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def location
|
|
409
|
+
linkto(:location) || self.parent
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def list_together
|
|
413
|
+
linkto :list_together
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def before_create
|
|
417
|
+
self.created_at ||= Time.now
|
|
418
|
+
super
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def before_destroy
|
|
422
|
+
self.safe_refresh if self.respond_to? :safe_refresh
|
|
423
|
+
self.children.each { |x| x.destroy unless x.has? :prized }
|
|
424
|
+
super
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def after_destroy
|
|
428
|
+
super
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def after_save
|
|
432
|
+
super
|
|
433
|
+
self.modified_at = Time.now
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def before_clone(copy); end
|
|
437
|
+
|
|
438
|
+
def after_clone(copy)
|
|
439
|
+
copy.untag :prized
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
# rubocop: disable Metrics/AbcSize
|
|
443
|
+
# rubocop: disable Metrics/MethodLength
|
|
444
|
+
def clone
|
|
445
|
+
return if Session.players.include? self # Don't clone players.
|
|
446
|
+
|
|
447
|
+
data = self.values.dup
|
|
448
|
+
data.delete :id
|
|
449
|
+
data.delete :attributes
|
|
450
|
+
data.delete :properties
|
|
451
|
+
copy = self.class.new(data)
|
|
452
|
+
before_clone(copy) if respond_to? :before_clone
|
|
453
|
+
copy.properties = self.properties
|
|
454
|
+
copy.save
|
|
455
|
+
copy.untag(*copy.tags)
|
|
456
|
+
copy.tag(*self.tags)
|
|
457
|
+
copy.mod(*self.modules)
|
|
458
|
+
copy.link(:original, self)
|
|
459
|
+
self.children.each { |x| copy << x.clone }
|
|
460
|
+
after_clone(copy) if respond_to? :after_clone
|
|
461
|
+
copy
|
|
462
|
+
end
|
|
463
|
+
# rubocop: enable Metrics/AbcSize
|
|
464
|
+
# rubocop: enable Metrics/MethodLength
|
|
465
|
+
|
|
466
|
+
def sysclone(klass = Inform::System::Object)
|
|
467
|
+
copy = klass.new self.short_name
|
|
468
|
+
copy.name = self.name
|
|
469
|
+
copy.properties[:source_id] = self.id
|
|
470
|
+
copy.properties = self.properties
|
|
471
|
+
copy.tags = self.nil_safe_tags
|
|
472
|
+
after_clone(copy) if respond_to? :after_clone
|
|
473
|
+
copy
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
ExportFileNameTemplate = '%<name>s-%<id>s.%<ext>s'.freeze
|
|
477
|
+
JsonOptions = {
|
|
478
|
+
include: %i[tagged modularized]
|
|
479
|
+
}.freeze
|
|
480
|
+
|
|
481
|
+
def export_json
|
|
482
|
+
return unless respond_to? :to_json
|
|
483
|
+
|
|
484
|
+
self.to_json(**JsonOptions).save(
|
|
485
|
+
format(ExportFileNameTemplate, name: self.to_s, id: self.id, ext: :json)
|
|
486
|
+
)
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def export_yaml
|
|
490
|
+
return unless self.respond_to? :to_yaml
|
|
491
|
+
|
|
492
|
+
self.to_yaml.save(
|
|
493
|
+
format(ExportFileNameTemplate, name: self.to_s, id: self.id, ext: :yaml)
|
|
494
|
+
)
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
XmlOptions = {
|
|
498
|
+
except: :id,
|
|
499
|
+
include: {
|
|
500
|
+
tagged: {
|
|
501
|
+
except: :id
|
|
502
|
+
},
|
|
503
|
+
modularized: {
|
|
504
|
+
except: :id
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}.freeze
|
|
508
|
+
XmlFileNameTemplate = '%<name>s-%<id>s.json'.freeze
|
|
509
|
+
|
|
510
|
+
def export_xml
|
|
511
|
+
return unless self.respond_to? :to_xml
|
|
512
|
+
|
|
513
|
+
self.to_xml(**XmlOptions).save(
|
|
514
|
+
format(ExportFileNameTemplate, name: self.to_s, id: self.id, ext: :xml)
|
|
515
|
+
)
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def encode_with(coder)
|
|
519
|
+
%w[id name short_name description properties links modularized tagged].each do |v|
|
|
520
|
+
coder[v] = self.send(v) if self.respond_to? v
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
# def init_with(coder)
|
|
525
|
+
# %w[ id name short_name description properties ].each do |v|
|
|
526
|
+
# m = "#{v}=".to_sym
|
|
527
|
+
# self.send(m, coder[v]) if self.respond_to? m
|
|
528
|
+
# end
|
|
529
|
+
# log.debug "Importing #{self.inspect}"
|
|
530
|
+
# log.debug "Importing #{self.to_hash.inspect}"
|
|
531
|
+
# log.debug " - #{(self.methods - ::Object.instance_methods).uniq.sort}"
|
|
532
|
+
# self
|
|
533
|
+
# end
|
|
534
|
+
end
|
|
535
|
+
# rubocop: enable Metrics/ClassLength
|
|
536
|
+
# class Object
|
|
537
|
+
end
|
|
538
|
+
# module Inform
|
|
539
|
+
|
|
540
|
+
# The ExtendedProperties class
|
|
541
|
+
class ExtendedProperties < Inform::Object
|
|
542
|
+
def init
|
|
543
|
+
has :proper
|
|
544
|
+
super
|
|
545
|
+
end
|
|
546
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# lib/story_teller/inform/relational/tag.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
|
+
# Tag
|
|
23
|
+
|
|
24
|
+
if defined?(Sequel::Migration)
|
|
25
|
+
# The TagSetup class
|
|
26
|
+
class TagSetup < Sequel::Migration
|
|
27
|
+
def up
|
|
28
|
+
# return if table_exists? :tag
|
|
29
|
+
log.debug "#up"
|
|
30
|
+
create_table? :tag do
|
|
31
|
+
primary_key :id
|
|
32
|
+
index :name
|
|
33
|
+
index :created_at
|
|
34
|
+
|
|
35
|
+
String :name, unique: true, null: false
|
|
36
|
+
DateTime :created_at
|
|
37
|
+
DateTime :modified_at
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def down
|
|
42
|
+
drop_table(:tag, cascade: true) if table_exists? :tag
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
# class TagSetup
|
|
46
|
+
end
|
|
47
|
+
# defined?(Sequel::Migration)
|
|
48
|
+
|
|
49
|
+
# module Inform
|
|
50
|
+
module Inform
|
|
51
|
+
# class Tag
|
|
52
|
+
class Tag < Sequel::Model
|
|
53
|
+
set_primary_key :id
|
|
54
|
+
def_column_accessor :created_at, :modified_at
|
|
55
|
+
def_column_accessor :name
|
|
56
|
+
|
|
57
|
+
def before_create
|
|
58
|
+
self.created_at ||= Time.now
|
|
59
|
+
super
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def after_create
|
|
63
|
+
super
|
|
64
|
+
Inform.attributes.reset
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def after_save
|
|
68
|
+
super
|
|
69
|
+
self.modified_at = Time.now.utc
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def to_s
|
|
73
|
+
name
|
|
74
|
+
end
|
|
75
|
+
alias to_str to_s
|
|
76
|
+
|
|
77
|
+
def <=>(other)
|
|
78
|
+
self.name <=> other.name
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.tidy
|
|
82
|
+
db << %(delete from tag where id in
|
|
83
|
+
(select a.id from tag a group by a.id, a.name
|
|
84
|
+
having ((select count(b.id) from tagged b
|
|
85
|
+
where b.tag_id = a.id) = 0)))
|
|
86
|
+
Inform.attributes.reset
|
|
87
|
+
return nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# rubocop: disable Style/FormatStringToken
|
|
91
|
+
def self.dirty
|
|
92
|
+
results = db.fetch %(select a.id, a.name from tag a group by a.id, a.name having
|
|
93
|
+
((select count(b.id) from tagged b where b.tag_id = a.id) = 0))
|
|
94
|
+
return nil if results.empty?
|
|
95
|
+
s = [format('%5s %20s', *results.first.keys)]
|
|
96
|
+
s.concat(results.collect { |row| format('%5d %20s', *row.values) })
|
|
97
|
+
s.join("\n")
|
|
98
|
+
end
|
|
99
|
+
# rubocop: enable Style/FormatStringToken
|
|
100
|
+
|
|
101
|
+
# rubocop: disable Style/FormatStringToken
|
|
102
|
+
def self.stats
|
|
103
|
+
results = db.fetch %(select a.*, count(a.id) as "number tagged"
|
|
104
|
+
from tag a, tagged b where b.tag_id = a.id group by a.id, a.name)
|
|
105
|
+
return nil if results.empty?
|
|
106
|
+
s = [format('%5s %20s %15s', *results.first.keys)]
|
|
107
|
+
s.concat(results.collect { |row| format('%5d %20s %15s', *row.values) })
|
|
108
|
+
s.join("\n")
|
|
109
|
+
end
|
|
110
|
+
# rubocop: enable Style/FormatStringToken
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
# module Inform
|
|
114
|
+
|
|
115
|
+
# module Inform
|
|
116
|
+
module Inform
|
|
117
|
+
module_function
|
|
118
|
+
|
|
119
|
+
def tag_klass
|
|
120
|
+
Inform::Tag
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Tagged
|
|
125
|
+
|
|
126
|
+
if defined?(Sequel::Migration)
|
|
127
|
+
# The TaggedSetup class
|
|
128
|
+
class TaggedSetup < Sequel::Migration
|
|
129
|
+
def up
|
|
130
|
+
log.debug "#up"
|
|
131
|
+
create_table? :tagged do
|
|
132
|
+
primary_key :id
|
|
133
|
+
foreign_key :object_id, :object, on_delete: :cascade
|
|
134
|
+
foreign_key :tag_id, :tag, on_delete: :cascade
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def down
|
|
139
|
+
drop_table(:tagged, cascade: true) if table_exists? :tagged
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
# class TaggedSetup
|
|
143
|
+
end
|
|
144
|
+
# defined?(Sequel::Migration)
|
|
145
|
+
|
|
146
|
+
# module Inform
|
|
147
|
+
module Inform
|
|
148
|
+
# The Tagged class
|
|
149
|
+
class Tagged < Sequel::Model
|
|
150
|
+
set_primary_key :id
|
|
151
|
+
end
|
|
152
|
+
end
|