kozeki 0.1.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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +5 -0
- data/Rakefile +8 -0
- data/bin/kozeki +5 -0
- data/lib/kozeki/build.rb +260 -0
- data/lib/kozeki/cli.rb +57 -0
- data/lib/kozeki/client.rb +48 -0
- data/lib/kozeki/collection.rb +136 -0
- data/lib/kozeki/collection_list.rb +27 -0
- data/lib/kozeki/config.rb +91 -0
- data/lib/kozeki/dsl.rb +65 -0
- data/lib/kozeki/filesystem.rb +46 -0
- data/lib/kozeki/item.rb +44 -0
- data/lib/kozeki/loader_chain.rb +20 -0
- data/lib/kozeki/local_filesystem.rb +83 -0
- data/lib/kozeki/markdown_loader.rb +52 -0
- data/lib/kozeki/queued_filesystem.rb +65 -0
- data/lib/kozeki/record.rb +48 -0
- data/lib/kozeki/source.rb +60 -0
- data/lib/kozeki/state.rb +326 -0
- data/lib/kozeki/version.rb +5 -0
- data/lib/kozeki.rb +8 -0
- data/sig/kozeki.rbs +4 -0
- metadata +140 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 030de1696d26b4f3cf6ed9616dd273f1610cbc63dea404e0250950d587fcd581
|
4
|
+
data.tar.gz: 3a6c8652c52482442ed17dc5c057912c6792b65f64989954b9497e9c7a6aaa04
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4e177182d9a52715721706494b858183c42c4391b4e8ac37890173bc15bcf1f6ec674d16ae5b00634f3ecefc9d211f08e10cacbf62e4db4ac60804ca408fa6da
|
7
|
+
data.tar.gz: e288bd18e158851513a2264ba5573e7980dfa8afc618633b805e8177473c2f8d51e7656b6a9ac3cfb23d20b183a1cf70887db355a00b7ccae20dc582de375229
|
data/.rspec
ADDED
data/CHANGELOG.md
ADDED
data/Rakefile
ADDED
data/bin/kozeki
ADDED
data/lib/kozeki/build.rb
ADDED
@@ -0,0 +1,260 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'json'
|
3
|
+
require 'kozeki/state'
|
4
|
+
require 'kozeki/item'
|
5
|
+
require 'kozeki/collection'
|
6
|
+
require 'kozeki/collection_list'
|
7
|
+
|
8
|
+
module Kozeki
|
9
|
+
class Build
|
10
|
+
|
11
|
+
# @param state [State]
|
12
|
+
def initialize(state:, source_filesystem:, destination_filesystem:, collection_list_included_prefix: nil, collection_options: [], loader:, events: nil, incremental_build:, logger: nil)
|
13
|
+
@state = state
|
14
|
+
|
15
|
+
@source_filesystem = source_filesystem
|
16
|
+
@destination_filesystem = destination_filesystem
|
17
|
+
@collection_list_included_prefix = collection_list_included_prefix
|
18
|
+
@collection_options = collection_options
|
19
|
+
@loader = loader
|
20
|
+
@loader_cache = {}
|
21
|
+
@logger = logger
|
22
|
+
|
23
|
+
@events = events
|
24
|
+
@incremental_build = incremental_build
|
25
|
+
|
26
|
+
@updated_files = []
|
27
|
+
@build_id = nil
|
28
|
+
end
|
29
|
+
|
30
|
+
attr_accessor :events, :incremental_build
|
31
|
+
attr_reader :updated_files
|
32
|
+
|
33
|
+
def inspect
|
34
|
+
"#<#{self.class.name}#{self.__id__}>"
|
35
|
+
end
|
36
|
+
|
37
|
+
def incremental_build_possible?
|
38
|
+
@state.build_exist?
|
39
|
+
end
|
40
|
+
|
41
|
+
def incremental_build?
|
42
|
+
incremental_build_possible? && @incremental_build
|
43
|
+
end
|
44
|
+
|
45
|
+
def full_build?
|
46
|
+
!incremental_build?
|
47
|
+
end
|
48
|
+
|
49
|
+
def perform
|
50
|
+
raise "can't reuse" if @build_id
|
51
|
+
if full_build?
|
52
|
+
@logger&.info "Starting full build"
|
53
|
+
else
|
54
|
+
@logger&.info "Starting incremental build"
|
55
|
+
end
|
56
|
+
|
57
|
+
@state.transaction do
|
58
|
+
process_prepare
|
59
|
+
process_events
|
60
|
+
end
|
61
|
+
@state.transaction do
|
62
|
+
process_items_remove
|
63
|
+
process_items_update
|
64
|
+
end
|
65
|
+
@state.transaction do
|
66
|
+
process_garbage
|
67
|
+
end
|
68
|
+
@state.transaction do
|
69
|
+
process_collections
|
70
|
+
end
|
71
|
+
@destination_filesystem.flush
|
72
|
+
@state.transaction do
|
73
|
+
process_commit
|
74
|
+
end
|
75
|
+
@destination_filesystem.flush
|
76
|
+
end
|
77
|
+
|
78
|
+
private def process_prepare
|
79
|
+
@logger&.debug "=== Prepare ==="
|
80
|
+
@state.clear! if full_build?
|
81
|
+
@build_id = @state.create_build
|
82
|
+
@logger&.debug "Build ID: #{@build_id.inspect}"
|
83
|
+
end
|
84
|
+
|
85
|
+
private def process_events
|
86
|
+
@logger&.debug "=== Process incoming events ==="
|
87
|
+
events = if full_build? || @events.nil?
|
88
|
+
fs_list = @source_filesystem.list_entries()
|
89
|
+
fs_list.map do |entry|
|
90
|
+
Filesystem::Event.new(op: :update, path: entry.path, time: full_build? ? nil : entry.mtime.floor(4))
|
91
|
+
end
|
92
|
+
else
|
93
|
+
@events.dup
|
94
|
+
end
|
95
|
+
|
96
|
+
if fs_list
|
97
|
+
seen_paths = {}
|
98
|
+
events.each do |event|
|
99
|
+
next unless event.op == :update
|
100
|
+
seen_paths[event.path] = true
|
101
|
+
end
|
102
|
+
@state.list_record_paths.reject do |path|
|
103
|
+
seen_paths[path]
|
104
|
+
end.each do |path|
|
105
|
+
events.push(Filesystem::Event.new(op: :delete, path:, time: nil))
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
events.each do |event|
|
110
|
+
@logger&.debug "> #{event.inspect}" if incremental_build? && @events
|
111
|
+
case event.op
|
112
|
+
when :update
|
113
|
+
if event.time
|
114
|
+
begin
|
115
|
+
record = @state.find_record_by_path!(event.path)
|
116
|
+
diff = event.time.to_f.floor(3) - record.mtime.to_f.floor(3)
|
117
|
+
if diff > 0.005
|
118
|
+
@logger&.debug "> #{event.inspect}"
|
119
|
+
@logger&.debug " #{record.mtime} (#{record.mtime.to_f.floor(3)}) < #{event.time} (#{event.time.to_f.floor(3)})"
|
120
|
+
else
|
121
|
+
next
|
122
|
+
end
|
123
|
+
rescue State::NotFound
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
source = load_source(event.path)
|
128
|
+
record = @state.save_record(source.to_record)
|
129
|
+
@state.set_record_pending_build_action(record, :update)
|
130
|
+
@logger&.info "ID change: #{event.path.inspect}; #{record.id_was.inspect} => #{record.id.inspect}" if record.id_was
|
131
|
+
when :delete
|
132
|
+
begin
|
133
|
+
record = @state.find_record_by_path!(event.path)
|
134
|
+
@state.set_record_pending_build_action(record, :remove)
|
135
|
+
rescue State::NotFound
|
136
|
+
end
|
137
|
+
else
|
138
|
+
raise "unknown op #{event.inspect}"
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
private def process_items_remove
|
144
|
+
@logger&.debug "=== Delete items for removed sources ==="
|
145
|
+
removed_records = @state.list_records_by_pending_build_action(:remove)
|
146
|
+
removed_records.each do |record|
|
147
|
+
if @state.list_records_by_id(record.id).any? { _1.pending_build_action == :update }
|
148
|
+
@logger&.warn "Skip deletion: #{record.id.inspect} (#{record.path.inspect})"
|
149
|
+
next
|
150
|
+
end
|
151
|
+
@logger&.info "Delete: #{record.id.inspect} (#{record.path.inspect})"
|
152
|
+
@destination_filesystem.delete(['items', "#{record.id}.json"])
|
153
|
+
@state.set_record_collections_pending(record.id, [])
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
private def process_items_update
|
158
|
+
@logger&.debug "=== Render items for updated sources ==="
|
159
|
+
updating_records = @state.list_records_by_pending_build_action(:update)
|
160
|
+
updating_records.each do |record|
|
161
|
+
@logger&.info "Render: #{record.id.inspect} (#{record.path.inspect})"
|
162
|
+
source = load_source(record.path)
|
163
|
+
|
164
|
+
# ID uniqueness check
|
165
|
+
_existing_record = begin
|
166
|
+
@state.find_record!(source.id)
|
167
|
+
rescue State::NotFound; nil
|
168
|
+
end
|
169
|
+
|
170
|
+
item = build_item(source)
|
171
|
+
@destination_filesystem.write(source.item_path, "#{JSON.generate(item.as_json)}\n")
|
172
|
+
@state.set_record_collections_pending(item.id, item.meta.fetch(:collections, []))
|
173
|
+
@updated_files << source.item_path
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
private def process_garbage
|
178
|
+
@logger&.debug "=== Collect garbages; items without source ==="
|
179
|
+
item_ids = @state.list_item_ids_for_garbage_collection
|
180
|
+
item_ids.each do |item_id|
|
181
|
+
@logger&.debug "Checking: #{item_id.inspect}"
|
182
|
+
records = @state.list_records_by_id(item_id)
|
183
|
+
if records.empty?
|
184
|
+
@logger&.info "Garbage: #{item_id.inspect}"
|
185
|
+
@state.mark_item_id_to_remove(item_id)
|
186
|
+
@state.set_record_collections_pending(item_id, [])
|
187
|
+
@destination_filesystem.delete(['items', "#{item_id}.json"])
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
private def process_collections
|
193
|
+
@logger&.debug "=== Render updated collections ==="
|
194
|
+
collections = @state.list_collection_names_pending
|
195
|
+
return if collections.empty?
|
196
|
+
|
197
|
+
collections.each do |collection_name|
|
198
|
+
records = @state.list_collection_records(collection_name)
|
199
|
+
record_count_was = @state.count_collection_records(collection_name)
|
200
|
+
|
201
|
+
collection = make_collection(collection_name, records)
|
202
|
+
collection.pages.each do |page|
|
203
|
+
@logger&.info "Render: Collection #{collection_name.inspect} (#{page.item_path.inspect})"
|
204
|
+
@destination_filesystem.write(page.item_path, "#{JSON.generate(page.as_json)}\n")
|
205
|
+
@updated_files << page.item_path
|
206
|
+
end
|
207
|
+
collection.item_paths_for_missing_pages(record_count_was).each do |path|
|
208
|
+
@logger&.info "Delete: Collection #{collection.inspect} (#{path.inspect})"
|
209
|
+
@destination_filesystem.delete(path)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
@logger&.info "Render: CollectionList"
|
214
|
+
collection_names = @state.list_collection_names_with_prefix(*@collection_list_included_prefix)
|
215
|
+
collection_list = CollectionList.new(collection_names)
|
216
|
+
@destination_filesystem.write(collection_list.item_path, "#{JSON.generate(collection_list.as_json)}\n")
|
217
|
+
@updated_files << collection_list.item_path
|
218
|
+
end
|
219
|
+
|
220
|
+
private def process_commit
|
221
|
+
@logger&.debug "=== Finishing build ==="
|
222
|
+
@logger&.debug "Flush pending actions"
|
223
|
+
@state.process_markers!
|
224
|
+
if full_build?
|
225
|
+
@logger&.info "Delete: untouched files from destination"
|
226
|
+
@destination_filesystem.retain_only(@updated_files)
|
227
|
+
end
|
228
|
+
@logger&.debug "Mark build completed"
|
229
|
+
@state.mark_build_completed(@build_id)
|
230
|
+
# TODO: @state.remove_old_builds
|
231
|
+
end
|
232
|
+
|
233
|
+
private def load_source(path)
|
234
|
+
@loader_cache[path] ||= begin
|
235
|
+
@logger&.debug("Load: #{path.inspect}")
|
236
|
+
@loader.try_read(path: path, filesystem: @source_filesystem) or raise "can't read #{path.inspect}"
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
private def build_item(source)
|
241
|
+
source.build_item
|
242
|
+
end
|
243
|
+
|
244
|
+
private def make_collection(name, records)
|
245
|
+
Collection.new(name, records, options: collection_option_for(name))
|
246
|
+
end
|
247
|
+
|
248
|
+
private def collection_option_for(collection_name)
|
249
|
+
retval = nil
|
250
|
+
len = -1
|
251
|
+
@collection_options.each do |x|
|
252
|
+
if collection_name.start_with?(x.prefix) && x.prefix.size > len
|
253
|
+
retval = x
|
254
|
+
len = x.prefix.size
|
255
|
+
end
|
256
|
+
end
|
257
|
+
retval
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
data/lib/kozeki/cli.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'thor'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
require 'kozeki/client'
|
6
|
+
require 'kozeki/config'
|
7
|
+
require 'kozeki/filesystem'
|
8
|
+
|
9
|
+
module Kozeki
|
10
|
+
class Cli < Thor
|
11
|
+
package_name 'kozeki'
|
12
|
+
|
13
|
+
desc 'build CONFIG_FILE', 'run a one-off build using CONFIG_FILE'
|
14
|
+
method_options :full => :boolean
|
15
|
+
method_options :events_from_stdin => :boolean
|
16
|
+
def build(config_file)
|
17
|
+
client = make_client(config_file)
|
18
|
+
client.build(
|
19
|
+
incremental_build: !options[:full],
|
20
|
+
events: options[:events_from_stdin] ? load_events_from_stdin() : nil,
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
desc 'watch CONFIG_FILE', 'run a continuous build by watching source filesystem using CONFIG_FILE'
|
25
|
+
def watch(config_file)
|
26
|
+
client = make_client(config_file)
|
27
|
+
client.watch
|
28
|
+
end
|
29
|
+
|
30
|
+
desc 'debug-state CONFIG_FILE', ''
|
31
|
+
def debug_state(config_file)
|
32
|
+
client = make_client(config_file)
|
33
|
+
state = State.open(path: client.config.state_path)
|
34
|
+
state.db.execute(%{select * from "records" order by "id" asc}).map { p record: _1 }
|
35
|
+
state.db.execute(%{select * from "collection_memberships" order by "collection" asc, "record_id" asc}).map { p collection_membership: _1 }
|
36
|
+
state.db.execute(%{select * from "item_ids" order by "id" asc}).map { p item_id: _1 }
|
37
|
+
end
|
38
|
+
|
39
|
+
no_commands do
|
40
|
+
private def make_client(config_file)
|
41
|
+
config = Config.load_file(config_file)
|
42
|
+
Client.new(config:)
|
43
|
+
end
|
44
|
+
|
45
|
+
private def load_events_from_stdin
|
46
|
+
j = JSON.parse($stdin.read, symbolize_names: true)
|
47
|
+
j.map do |x|
|
48
|
+
Filesystem::Event.new(
|
49
|
+
op: x.fetch(:op).to_sym,
|
50
|
+
path: x.fetch(:path),
|
51
|
+
time: nil,
|
52
|
+
)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'kozeki/state'
|
3
|
+
require 'kozeki/build'
|
4
|
+
|
5
|
+
module Kozeki
|
6
|
+
class Client
|
7
|
+
# @param config [Config]
|
8
|
+
def initialize(config:)
|
9
|
+
@config = config
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :config
|
13
|
+
|
14
|
+
def build(incremental_build: true, events: nil)
|
15
|
+
begin
|
16
|
+
state = State.open(path: config.state_path)
|
17
|
+
build = Build.new(
|
18
|
+
state: state,
|
19
|
+
source_filesystem: @config.source_filesystem,
|
20
|
+
destination_filesystem: @config.destination_filesystem,
|
21
|
+
collection_list_included_prefix: @config.collection_list_included_prefix,
|
22
|
+
collection_options: @config.collection_options,
|
23
|
+
loader: @config.loader,
|
24
|
+
incremental_build:,
|
25
|
+
events:,
|
26
|
+
logger: @config.logger,
|
27
|
+
)
|
28
|
+
build.perform
|
29
|
+
ensure
|
30
|
+
state&.close
|
31
|
+
end
|
32
|
+
|
33
|
+
@config.after_build_callbacks.each do |cb|
|
34
|
+
cb.call(build)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def watch
|
39
|
+
build(incremental_build: true, events: nil)
|
40
|
+
stop = @config.source_filesystem.watch do |events|
|
41
|
+
build(incremental_build: true, events: events)
|
42
|
+
end
|
43
|
+
sleep
|
44
|
+
ensure
|
45
|
+
stop&.call
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module Kozeki
|
5
|
+
class Collection
|
6
|
+
def initialize(name, records, options: nil)
|
7
|
+
raise ArgumentError, "name cannot include /" if name.include?('/')
|
8
|
+
@name = name
|
9
|
+
@records = records
|
10
|
+
@options = options
|
11
|
+
|
12
|
+
@records_sorted = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :name, :records, :options
|
16
|
+
|
17
|
+
def records_sorted
|
18
|
+
@records_sorted ||= @records.sort_by do |record|
|
19
|
+
record.timestamp&.then { -_1.to_i } || 0
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def total_pages
|
24
|
+
@total_pages ||= calculate_total_pages(records.size)
|
25
|
+
end
|
26
|
+
|
27
|
+
def pages
|
28
|
+
case
|
29
|
+
when records.empty?
|
30
|
+
[]
|
31
|
+
when options&.paginate && options&.max_items
|
32
|
+
total_pages.times.map do |i|
|
33
|
+
Page.new(parent: self, page: i+1)
|
34
|
+
end
|
35
|
+
else
|
36
|
+
[Page.new(parent: self, page: nil)]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def item_path_for_page(pagenum)
|
41
|
+
case pagenum
|
42
|
+
when 0
|
43
|
+
raise "[bug] page is 1-origin"
|
44
|
+
when nil, 1
|
45
|
+
['collections', "#{name}.json"]
|
46
|
+
else
|
47
|
+
['collections', "#{name}", "page-#{pagenum}.json"]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def item_paths_for_missing_pages(item_count_was)
|
52
|
+
total_pages_was = calculate_total_pages(item_count_was)
|
53
|
+
if (total_pages_was - total_pages) > 0
|
54
|
+
(total_pages+1..total_pages_was).map do |pagenum|
|
55
|
+
item_path_for_page(pagenum)
|
56
|
+
end
|
57
|
+
else
|
58
|
+
[]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def calculate_total_pages(count)
|
63
|
+
if options&.paginate && options&.max_items
|
64
|
+
count.divmod(options.max_items).then {|(a,b)| a + (b>0 ? 1 : 0) }
|
65
|
+
else
|
66
|
+
count > 0 ? 1 : 0
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class Page
|
71
|
+
def initialize(parent:, page:)
|
72
|
+
@parent = parent
|
73
|
+
@page = page
|
74
|
+
|
75
|
+
@total_pages = nil
|
76
|
+
end
|
77
|
+
|
78
|
+
def name; @parent.name; end
|
79
|
+
def options; @parent.options; end
|
80
|
+
def total_pages; @parent.total_pages; end
|
81
|
+
|
82
|
+
def records
|
83
|
+
case @page
|
84
|
+
when nil
|
85
|
+
@parent.records_sorted
|
86
|
+
when 0
|
87
|
+
raise "[bug] page is 1-origin"
|
88
|
+
else
|
89
|
+
@parent.records_sorted[(@page - 1) * options.max_items, options.max_items]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def as_json
|
94
|
+
{
|
95
|
+
kind: 'collection',
|
96
|
+
name: name,
|
97
|
+
items: records.map do |record|
|
98
|
+
{
|
99
|
+
id: record.id,
|
100
|
+
path: ['items', "#{record.id}.json"].join('/'),
|
101
|
+
meta: options&.meta_keys ? record.meta.slice(*options.meta_keys) : record.meta,
|
102
|
+
}
|
103
|
+
end.then do |page|
|
104
|
+
options&.max_items ? page[0, options.max_items] : page
|
105
|
+
end,
|
106
|
+
}.tap do |j|
|
107
|
+
j[:page] = page_info if @page
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def item_path
|
112
|
+
@parent.item_path_for_page(@page)
|
113
|
+
end
|
114
|
+
|
115
|
+
def page_info
|
116
|
+
return nil unless @page
|
117
|
+
prev_page = 1 < @page ? @parent.item_path_for_page(@page-1) : nil
|
118
|
+
next_page = @page < total_pages ? @parent.item_path_for_page(@page+1) : nil
|
119
|
+
i = {
|
120
|
+
self: @page,
|
121
|
+
total_pages:,
|
122
|
+
first: @parent.item_path_for_page(1).join('/'),
|
123
|
+
last: @parent.item_path_for_page(total_pages).join('/'),
|
124
|
+
prev: prev_page&.join('/'),
|
125
|
+
next: next_page&.join('/'),
|
126
|
+
}
|
127
|
+
if @page == 1
|
128
|
+
i[:pages] = (1..total_pages).map do |pagenum|
|
129
|
+
@parent.item_path_for_page(pagenum).join('/')
|
130
|
+
end
|
131
|
+
end
|
132
|
+
i
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kozeki
|
4
|
+
class CollectionList
|
5
|
+
def initialize(names)
|
6
|
+
@names = names
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :names
|
10
|
+
|
11
|
+
def as_json
|
12
|
+
{
|
13
|
+
kind: 'collection_list',
|
14
|
+
collections: names.sort.map do |name|
|
15
|
+
{
|
16
|
+
name:,
|
17
|
+
path: ['collections', "#{name}.json"].join('/'),
|
18
|
+
}
|
19
|
+
end,
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
def item_path
|
24
|
+
['collections.json']
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'kozeki/dsl'
|
3
|
+
require 'kozeki/loader_chain'
|
4
|
+
require 'kozeki/local_filesystem'
|
5
|
+
require 'kozeki/markdown_loader'
|
6
|
+
require 'logger'
|
7
|
+
|
8
|
+
module Kozeki
|
9
|
+
class Config
|
10
|
+
CollectionOptions = Struct.new(:prefix, :max_items, :paginate, :meta_keys)
|
11
|
+
|
12
|
+
def initialize(options)
|
13
|
+
@options = options
|
14
|
+
@source_filesystem = nil
|
15
|
+
@destination_filesystem = nil
|
16
|
+
@loader = nil
|
17
|
+
@logger = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.load_file(path)
|
21
|
+
new(Dsl.load_file(path).options)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.configure(&block)
|
25
|
+
new(Dsl.eval(&block).options)
|
26
|
+
end
|
27
|
+
|
28
|
+
def [](k)
|
29
|
+
@options[k]
|
30
|
+
end
|
31
|
+
|
32
|
+
def fetch(k,*args)
|
33
|
+
@options.fetch(k,*args)
|
34
|
+
end
|
35
|
+
|
36
|
+
def base_directory
|
37
|
+
@options.fetch(:base_directory, '.')
|
38
|
+
end
|
39
|
+
|
40
|
+
def source_directory
|
41
|
+
@options.fetch(:source_directory)
|
42
|
+
end
|
43
|
+
|
44
|
+
def destination_directory
|
45
|
+
@options.fetch(:destination_directory)
|
46
|
+
end
|
47
|
+
|
48
|
+
def cache_directory
|
49
|
+
@options.fetch(:cache_directory, nil)
|
50
|
+
end
|
51
|
+
|
52
|
+
def collection_list_included_prefix
|
53
|
+
@options.fetch(:collection_list_included_prefix, nil)
|
54
|
+
end
|
55
|
+
|
56
|
+
def collection_options
|
57
|
+
@options.fetch(:collection_options, [])
|
58
|
+
end
|
59
|
+
|
60
|
+
def metadata_decorators
|
61
|
+
@options.fetch(:metadata_decorators, [])
|
62
|
+
end
|
63
|
+
|
64
|
+
def after_build_callbacks
|
65
|
+
@options.fetch(:after_build_callbacks, [])
|
66
|
+
end
|
67
|
+
|
68
|
+
def state_path
|
69
|
+
cache_directory ? File.join(File.expand_path(cache_directory, base_directory), 'state.sqlite3') : ':memory:'
|
70
|
+
end
|
71
|
+
|
72
|
+
def source_filesystem
|
73
|
+
@source_filesystem ||= @options.fetch(:source_filesystem) { LocalFilesystem.new(File.expand_path(source_directory, base_directory)) }
|
74
|
+
end
|
75
|
+
|
76
|
+
def destination_filesystem
|
77
|
+
@destination_filesystem ||= @options.fetch(:destination_filesystem) { LocalFilesystem.new(File.expand_path(destination_directory, base_directory)) }
|
78
|
+
end
|
79
|
+
|
80
|
+
def loader
|
81
|
+
@loader ||= LoaderChain.new(
|
82
|
+
loaders: [MarkdownLoader],
|
83
|
+
decorators: metadata_decorators,
|
84
|
+
)
|
85
|
+
end
|
86
|
+
|
87
|
+
def logger
|
88
|
+
@logger ||= Logger.new($stdout)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|