paneron-register 0.2.0 → 0.3.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.
@@ -1,21 +1,286 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "yaml"
4
+ require "set"
4
5
 
5
6
  module Paneron
6
7
  module Register
7
8
  module Raw
8
9
  class Register
9
- attr_reader :register_path, :register_yaml_path
10
+ include Writeable
11
+ include Validatable
10
12
 
11
- def initialize(register_path)
12
- self.class.validate_path(register_path)
13
+ attr_reader :git_client, :git_url, :git_branch, :git_remote_name
14
+ attr_accessor :register_path
15
+
16
+ def initialize(
17
+ register_path,
18
+ git_url: nil,
19
+ update_git: nil,
20
+ git_remote_name: nil,
21
+ git_branch: nil
22
+ )
23
+ @old_git_url = @git_url = git_url
13
24
  @register_path = register_path
14
- @register_yaml_path = File.join(register_path,
15
- REGISTER_METADATA_FILENAME)
16
- @data_set_names = nil
25
+ setup_git(
26
+ git_url: git_url,
27
+ path: register_path,
28
+ git_remote_name: git_remote_name,
29
+ git_branch: git_branch,
30
+ update: update_git,
31
+ )
32
+
33
+ @old_path = @register_path
17
34
  @data_sets = {}
35
+ @item_classes = {}
36
+ @items = {}
18
37
  @metadata = nil
38
+ @git_save_fn = proc {}
39
+ end
40
+
41
+ # Defer all mkdir until #save_sequence
42
+ def setup_git(
43
+ git_url: nil,
44
+ path: nil,
45
+ git_remote_name: nil,
46
+ git_branch: nil,
47
+ update: nil
48
+ )
49
+ require "git"
50
+ self.class.setup_cache_path
51
+
52
+ @git_remote_name = if git_remote_name.nil? || git_remote_name.empty?
53
+ "origin"
54
+ else
55
+ git_remote_name
56
+ end
57
+
58
+ @git_branch = if git_branch.nil? || git_branch.empty?
59
+ "main"
60
+ else
61
+ git_branch
62
+ end
63
+
64
+ if git_url.nil? && path.nil?
65
+ raise Paneron::Register::Error,
66
+ "Must supply either git_url or path."
67
+ end
68
+
69
+ repo_path = if path.nil?
70
+ self.class.calculate_repo_cache_path(git_url)
71
+ else
72
+ path
73
+ end
74
+
75
+ #------------------------------------
76
+ # | dir | Git.open(dir)
77
+ # no |exists |
78
+ # git |-------|----------------------
79
+ # url | dir | mkdirp &&
80
+ # |!exists| Git.open(dir) ? Git.init(dir)
81
+ #------------------------------------
82
+ # | dir | Git.open(dir) &&
83
+ # has |exists | remote? ? check remote : add remote
84
+ # git |-------|----------------------
85
+ # url | dir | Git.clone(url, dir) ||
86
+ # |!exists| mkdirp && Git.open(dir) ? Git.init(dir) &&
87
+ # | | add remote
88
+ #------------------------------------
89
+
90
+ if git_url.nil?
91
+ if File.exist?(repo_path)
92
+ # No remote, but local repo path exists.
93
+ # Simply open it as a Git repo.
94
+ @git_save_fn = nil
95
+
96
+ begin
97
+ @git_client = self.class.open_git_repo(repo_path)
98
+ log_change_git_remote(nil)
99
+ change_git_remote(nil)
100
+ rescue ArgumentError => e
101
+ if /not in a git working tree/.match?(e.message)
102
+ @git_save_fn = proc {
103
+ @git_client = self.class.init_git_repo(repo_path,
104
+ initial_branch: @git_branch)
105
+ log_change_git_remote(nil)
106
+ change_git_remote(nil)
107
+ }
108
+ else
109
+ raise e
110
+ end
111
+ end
112
+ else
113
+ # No remote, and local repo path does not exist.
114
+ git_init_fn = proc {
115
+ FileUtils.mkdir_p(repo_path)
116
+ @git_client = self.class.init_git_repo(repo_path, initial_branch: @git_branch)
117
+ log_change_git_remote(nil)
118
+ change_git_remote(nil)
119
+ }
120
+
121
+ # Defer creation of directory until #save_sequence
122
+ @git_client = nil
123
+ @git_save_fn = git_init_fn
124
+
125
+ end
126
+ elsif File.exist?(repo_path)
127
+ # Has remote, as well as local repo path.
128
+ @git_save_fn = nil
129
+
130
+ git_fn = proc {
131
+ # Check if remote matches the provided git_url
132
+ if !@git_client.remote(@git_remote_name).url.nil? && @git_client.remote(@git_remote_name).url != git_url
133
+
134
+ raise Paneron::Register::Error,
135
+ "Git remote @ #{clone_path} already exists " \
136
+ "(#{@git_client.remote(@git_remote_name).url}) " \
137
+ "but does not match provided URL (#{git_url}).\n" \
138
+ "Instead, use `r = #{self}.new(\"#{path}\")` and "\
139
+ "`r.git_url = \"#{git_url}\"` to change its Git URL."
140
+ end
141
+ log_change_git_remote(git_url)
142
+ change_git_remote(git_url)
143
+
144
+ # Pull-rebase to update it
145
+ if update
146
+ @git_client.pull(
147
+ nil, nil, rebase: true
148
+ )
149
+ end
150
+ }
151
+
152
+ begin
153
+ @git_client = self.class.open_git_repo(repo_path)
154
+ git_fn.call
155
+ rescue ArgumentError => e
156
+ if /not in a git working tree/.match?(e.message)
157
+ @git_save_fn = proc {
158
+ @git_client = self.class.init_git_repo(repo_path,
159
+ initial_branch: @git_branch)
160
+ git_fn.call
161
+ }
162
+ else
163
+ raise e
164
+ end
165
+ end
166
+
167
+ else
168
+ git_clone_fn = proc {
169
+ begin
170
+ @git_client = self.class.clone_git_repo(git_url, repo_path)
171
+ change_git_remote(git_url)
172
+ rescue Git::TimeoutError => e
173
+ e.result.tap do |_r|
174
+ warn "Timed out trying to clone #{repo_url}."
175
+ raise e
176
+ end
177
+ end
178
+ }
179
+
180
+ # rubocop:disable Style/IdenticalConditionalBranches
181
+ # URL changed. Use save fn.
182
+ if git_url_changed?(git_url)
183
+ log_change_git_remote(git_url)
184
+ @git_client = nil
185
+ @git_save_fn = git_clone_fn
186
+ else
187
+ # Path is nil. Clone repo.
188
+ log_change_git_remote(git_url)
189
+ @git_save_fn = nil
190
+ git_clone_fn.call
191
+ end
192
+ # rubocop:enable Style/IdenticalConditionalBranches
193
+ end
194
+ end
195
+
196
+ def register_yaml_path
197
+ File.join(register_path,
198
+ REGISTER_METADATA_FILENAME)
199
+ end
200
+
201
+ def parent; nil; end
202
+
203
+ def self.name
204
+ "Register"
205
+ end
206
+
207
+ def save_sequence
208
+ # Save self
209
+ require "fileutils"
210
+
211
+ # Move old register to new path
212
+ if File.directory?(@old_path) && @old_path != self_path
213
+ FileUtils.mv(@old_path, self_path)
214
+ @old_path = self_path
215
+ else
216
+ FileUtils.mkdir_p(self_path)
217
+ end
218
+
219
+ if @git_client.nil?
220
+ @git_save_fn.call
221
+ end
222
+
223
+ if @metadata.nil? || @metadata.empty?
224
+ File.write(register_yaml_path, self.class.metadata_template.to_yaml)
225
+ else
226
+ File.write(register_yaml_path, metadata.to_yaml)
227
+ end
228
+
229
+ # Save data sets
230
+ data_set_names.each do |data_set_name|
231
+ new_thing = data_sets(data_set_name)
232
+ new_thing.register = self
233
+ new_thing.save
234
+ end
235
+ # else
236
+ # raise Paneron::Register::Error, "Register is not valid"
237
+ # end
238
+ end
239
+
240
+ def title=(new_title)
241
+ metadata["title"] = new_title.to_s
242
+ end
243
+
244
+ def title
245
+ metadata["title"]
246
+ end
247
+
248
+ def self_path
249
+ register_path
250
+ end
251
+
252
+ # TODO: Expand validation to include data set metadata?
253
+ # TODO: What is considered valid?
254
+ def is_valid?
255
+ true
256
+ end
257
+
258
+ def add_data_sets(*new_data_sets)
259
+ new_data_sets = [new_data_sets] unless new_data_sets.is_a?(Enumerable)
260
+ new_data_sets.each do |data_set|
261
+ data_set.set_register(self)
262
+ @data_sets[data_set.data_set_name] = data_set
263
+ metadata["datasets"].merge!(
264
+ { data_set.data_set_name => true },
265
+ )
266
+ end
267
+ end
268
+
269
+ def spawn_data_set(
270
+ data_set_name,
271
+ metadata: {},
272
+ paneron_metadata: {}
273
+ )
274
+ new_data_set = Paneron::Register::Raw::DataSet.new(
275
+ File.join(register_path, data_set_name),
276
+ register: self,
277
+ )
278
+
279
+ new_data_set.merge_metadata(metadata)
280
+ new_data_set.merge_paneron_metadata(paneron_metadata)
281
+ add_data_sets(new_data_set)
282
+
283
+ new_data_set
19
284
  end
20
285
 
21
286
  def self.local_cache_path
@@ -27,7 +292,7 @@ module Paneron
27
292
  )
28
293
  else
29
294
  File.join(
30
- Dir.exist?(ENV["XDG_CACHE_HOME"]) ? ENV["XDG_CACHE_HOME"] : "~/.cache",
295
+ Dir.exist?(ENV["XDG_CACHE_HOME"].to_s) ? ENV["XDG_CACHE_HOME"] : "~/.cache",
31
296
  "ruby-paneron-register",
32
297
  )
33
298
  end
@@ -47,59 +312,64 @@ module Paneron
47
312
  Base64.encode64(digest).tr("+/= ", "_-")[0..16]
48
313
  end
49
314
 
50
- def self.from_git(repo_url, update: true)
51
- require "git"
52
- setup_cache_path
315
+ # Basically .new but calls #save at the end
316
+ def self.generate(
317
+ register_path,
318
+ git_url: nil,
319
+ git_branch: nil,
320
+ git_remote_name: nil
321
+ )
322
+ new(
323
+ register_path,
324
+ git_url: git_url,
325
+ git_branch: git_branch,
326
+ git_remote_name: git_remote_name,
327
+ ).save
328
+ end
329
+
330
+ def self.calculate_repo_cache_name(repo_url)
331
+ "#{File.basename(repo_url)}-#{calculate_repo_cache_hash(repo_url)}"
332
+ end
333
+
334
+ def self.calculate_repo_cache_path(repo_url)
53
335
  repo_cache_name =
54
- "#{File.basename(repo_url)}-#{calculate_repo_cache_hash(repo_url)}"
336
+ calculate_repo_cache_name(repo_url)
55
337
 
56
338
  # Check if repo is already cloned
57
- full_local_cache_path = File.join(local_cache_path, repo_cache_name)
58
- g = begin
59
- if File.exist?(full_local_cache_path)
60
- _g = Git.open(full_local_cache_path)
339
+ File.join(local_cache_path, repo_cache_name)
340
+ end
61
341
 
62
- # Pull-rebase to update it
63
- if update
64
- _g.pull(
65
- nil, nil, rebase: true
66
- )
67
- end
68
- _g
69
- else
70
- Git.clone(
71
- repo_url,
72
- repo_cache_name,
73
- path: local_cache_path,
74
- # timeout: 30,
75
- )
76
- end
77
- rescue Git::TimeoutError => e
78
- e.result.tap do |_r|
79
- warn "Timed out trying to clone #{repo_url}."
80
- raise e
81
- end
82
- end
342
+ def git_url=(new_url)
343
+ setup_git(git_url: new_url, path: self_path)
344
+ end
83
345
 
84
- new(g.dir.path)
346
+ def self.from_git(repo_url, path: nil, update: true)
347
+ new(path, git_url: repo_url, update_git: true)
85
348
  end
86
349
 
87
350
  REGISTER_METADATA_FILENAME = "/paneron.yaml"
88
351
 
89
- def self.validate_path(register_path)
90
- unless File.exist?(register_path)
352
+ def self.validate_path_before_saving
353
+ false
354
+ end
355
+
356
+ def self.validate_path(path)
357
+ unless File.exist?(path)
91
358
  raise Paneron::Register::Error,
92
- "Register path does not exist"
359
+ "#{name} path (#{path}) does not exist"
93
360
  end
94
- unless File.directory?(register_path)
361
+
362
+ unless File.directory?(path)
95
363
  raise Paneron::Register::Error,
96
- "Register path is not a directory"
364
+ "#{name} path (#{path}) is not a directory"
97
365
  end
98
- unless File.exist?(File.join(
99
- register_path, REGISTER_METADATA_FILENAME
100
- ))
366
+
367
+ register_file = File.join(
368
+ path, REGISTER_METADATA_FILENAME
369
+ )
370
+ unless File.exist?(register_file)
101
371
  raise Paneron::Register::Error,
102
- "Register metadata file does not exist"
372
+ "Register metadata file (#{register_file}) does not exist"
103
373
  end
104
374
  end
105
375
 
@@ -110,16 +380,21 @@ module Paneron
110
380
  )
111
381
  end
112
382
 
113
- def data_set_names
114
- @data_set_names ||= Dir.glob(
115
- File.join(
116
- register_path,
117
- "*#{Paneron::Register::Raw::DataSet::DATA_SET_METADATA_FILENAME}",
118
- ),
119
- )
120
- .map do |file|
383
+ def data_set_names(refresh: false)
384
+ if refresh || @data_sets.empty?
385
+ Dir.glob(
386
+ File.join(
387
+ register_path,
388
+ "*#{Paneron::Register::Raw::DataSet::DATA_SET_METADATA_FILENAME}",
389
+ ),
390
+ )
391
+ .map do |file|
121
392
  File.basename(File.dirname(file))
122
- end
393
+ end.to_set
394
+ else
395
+ @data_sets.keys
396
+ end
397
+ # @data_set_names ||= Dir.glob(
123
398
  end
124
399
 
125
400
  def data_set_path(data_set_name)
@@ -127,27 +402,141 @@ module Paneron
127
402
  end
128
403
 
129
404
  def metadata
130
- @metadata ||= YAML.safe_load_file(register_yaml_path)
405
+ @metadata ||= begin
406
+ YAML.safe_load_file(
407
+ register_yaml_path,
408
+ permitted_classes: [Time, Date, DateTime],
409
+ )
410
+ rescue Errno::ENOENT
411
+ self.class.metadata_template
412
+ end
413
+ end
414
+
415
+ def metadata=(metadata)
416
+ @metadata = metadata
131
417
  end
132
418
 
133
- def data_sets(data_set_name = nil)
419
+ def data_sets(data_set_name = nil, refresh: false)
134
420
  if data_set_name.nil?
135
- data_set_names.reduce({}) do |acc, data_set_name|
136
- acc[data_set_name] = data_sets(data_set_name)
137
- acc
138
- end
421
+ @data_sets = if !refresh && !@data_sets.empty?
422
+ @data_sets
423
+ else
424
+ data_set_names(refresh: refresh).reduce({}) do |acc, data_set_name|
425
+ acc[data_set_name] = data_sets(data_set_name)
426
+ acc
427
+ end
428
+ end
429
+ elsif refresh
430
+ data_sets(refresh: true)[data_set_name]
139
431
  else
140
432
  @data_sets[data_set_name] ||=
141
- Paneron::Register::Raw::DataSet.new(register_path,
142
- data_set_name)
433
+ Paneron::Register::Raw::DataSet.new(
434
+ File.join(register_path, data_set_name),
435
+ register: self,
436
+ )
143
437
  end
144
438
  end
145
439
 
440
+ # @return Hash of { data_set_name => { item_class_name => ItemClass }}
441
+ # - ["data_set_name"]["item_class_name"]
442
+ def item_classes(data_set_name = nil, item_class_name = nil, refresh: false)
443
+ if data_set_name.nil? && item_class_name.nil?
444
+ @item_classes = if !refresh && !@item_classes.empty?
445
+ @item_classes
446
+ else
447
+ data_sets.reduce({}) do |acc, (ddata_set_name, data_set)|
448
+ acc[ddata_set_name] ||= {}
449
+ data_set.item_class_names.each do |item_klass_name|
450
+ acc[ddata_set_name][item_klass_name] =
451
+ item_classes(ddata_set_name, item_klass_name)
452
+ end
453
+ acc
454
+ end
455
+ end
456
+ elsif item_class_name.nil?
457
+ item_classes(refresh: refresh)[data_set_name]
458
+ elsif refresh
459
+ item_classes(refresh: true)[data_set_name][item_class_name]
460
+ else
461
+ @item_classes[data_set_name] ||= {}
462
+ @item_classes[data_set_name][item_class_name] ||=
463
+ Paneron::Register::Raw::ItemClass.new(
464
+ File.join(data_set_path(data_set_name), item_class_name),
465
+ data_set: data_sets[data_set_name],
466
+ )
467
+ end
468
+ end
469
+
470
+ # @return Hash of { item_uuid => Item }
471
+ # - ["uuid"]
472
+ def items(item_uuid = nil, refresh: false)
473
+ @items = if !refresh && !@items.empty?
474
+ @items
475
+ else
476
+ data_sets(refresh: refresh).reduce({}) do |acc, (_ddata_set_name, data_set)|
477
+ data_set.items.each do |iitem_uuid, item|
478
+ acc[iitem_uuid] = item
479
+ end
480
+ acc
481
+ end
482
+ end
483
+
484
+ if item_uuid.nil?
485
+ @items
486
+ else
487
+ @items[item_uuid]
488
+ end
489
+ end
490
+
491
+ def self.metadata_template
492
+ {
493
+ "title" => "",
494
+ "datasets" => {
495
+ # "data-set-1" => true,
496
+ },
497
+ }
498
+ end
499
+
500
+ private
501
+
502
+ def log_change_git_remote(new_url)
503
+ @old_git_url = @git_url
504
+ @git_url = new_url
505
+ end
506
+
507
+ def change_git_remote(new_url, git_client: @git_client)
508
+ if !git_client.remote(@git_remote_name).url.nil?
509
+ git_client.remove_remote(@git_remote_name)
510
+ if !new_url.nil?
511
+ git_client.add_remote(@git_remote_name)
512
+ end
513
+ end
514
+ end
515
+
516
+ def git_url_changed?(url = @git_url)
517
+ @old_git_url != url
518
+ end
519
+
146
520
  def data_set_lutamls
147
521
  data_sets.map do |_data_set_name, data_set|
148
522
  data_set.to_lutaml
149
523
  end
150
524
  end
525
+
526
+ # For abstraction
527
+ class << self
528
+ def clone_git_repo(git_url, repo_path)
529
+ Git.clone(git_url, repo_path)
530
+ end
531
+
532
+ def open_git_repo(repo_path)
533
+ Git.open(repo_path)
534
+ end
535
+
536
+ def init_git_repo(repo_path, initial_branch: nil)
537
+ Git.init(repo_path, initial_branch: initial_branch)
538
+ end
539
+ end
151
540
  end
152
541
  end
153
542
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "yaml"
4
-
5
3
  require "lutaml/model"
6
4
 
7
5
  module Paneron
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Paneron
4
+ module Register
5
+ module RootFinder
6
+ # def self.included(base)
7
+ # base.class_eval do
8
+ # end
9
+ # end
10
+
11
+ def register
12
+ parent.register
13
+ end
14
+
15
+ def git_client
16
+ parent.git_client
17
+ end
18
+
19
+ def git_url
20
+ parent.git_url
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Paneron
4
+ module Register
5
+ module Validatable
6
+ # def self.included(base)
7
+ # base.class_eval do
8
+ # end
9
+ # end
10
+
11
+ def path_valid?
12
+ self.class.validate_path(self_path)
13
+ true
14
+ rescue Paneron::Register::Error => e
15
+ errors << e.message
16
+ warn "#{self.class.name} is not path-valid:\n#{errors.map do |e|
17
+ " - #{e}"
18
+ end.join("\n")}"
19
+ false
20
+ end
21
+
22
+ def valid?
23
+ @errors = []
24
+ # Taking advantage of side-effects in #is_valid?
25
+ # before validate_path happens:
26
+ is_valid?
27
+ rescue Paneron::Register::Error => e
28
+ errors << e.message
29
+ warn "#{self.class.name} is not valid:\n#{errors.map do |e|
30
+ " - #{e}"
31
+ end.join("\n")}"
32
+ false
33
+ end
34
+
35
+ def errors
36
+ @errors ||= []
37
+ end
38
+ end
39
+ end
40
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Paneron
4
4
  module Register
5
- VERSION = "0.2.0"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end