kozeki 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|