paneron-register 0.2.0 → 0.3.0

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