Module: LucaRecord::IO::ClassMethods

Defined in:
lib/luca_record/io.rb

Instance Method Summary collapse

Instance Method Details

#add_status!(id, status, basedir = @dirname) ⇒ Object



203
204
205
206
207
208
209
210
# File 'lib/luca_record/io.rb', line 203

def add_status!(id, status, basedir = @dirname)
  path = abs_path(basedir) / id2path(id)
  origin = YAML.safe_load(File.read(path), permitted_classes: [Date])
  newline = { status => DateTime.now.to_s }
  origin['status'] = [] if origin['status'].nil?
  origin['status'] << newline
  File.write(path, YAML.dump(origin.sort.to_h))
end

#all(basedir = @dirname) ⇒ Object

retrieve all data



117
118
119
120
121
122
123
# File 'lib/luca_record/io.rb', line 117

def all(basedir = @dirname)
  return enum_for(:all, basedir) unless block_given?

  open_all(basedir) do |f|
    yield load_data(f)
  end
end

#asof(year, month = nil, day = nil, basedir = @dirname) ⇒ Object

search date based record.

  • data hash

  • data id. Array like [2020H, V001]



86
87
88
89
90
# File 'lib/luca_record/io.rb', line 86

def asof(year, month = nil, day = nil, basedir = @dirname)
  return enum_for(:search, year, month, day, nil, basedir) unless block_given?

  search(year, month, day, nil, basedir) { |data, path| yield data, path }
end

#change_codes(id, new_codes, basedir = @dirname) ⇒ Object

change filename with new code set



242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/luca_record/io.rb', line 242

def change_codes(id, new_codes, basedir = @dirname)
  raise 'invalid id' if id.split('/').length != 2

  newfile = new_codes.empty? ? id : id + '-' + new_codes.join('-')
  Dir.chdir(abs_path(basedir)) do
    origin = Dir.glob("#{id}*")
    raise 'duplicated files' if origin.length != 1

    File.rename(origin.first, newfile)
  end
  newfile
end

#create(obj, date: nil, codes: nil, basedir: @dirname) ⇒ Object

create record both of uuid/date identified.



139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/luca_record/io.rb', line 139

def create(obj, date: nil, codes: nil, basedir: @dirname)
  validate_keys(obj)
  if date
    create_record(obj, date, codes, basedir)
  else
    obj['id'] = LucaSupport::Code.issue_random_id
    open_hashed(basedir, obj['id'], 'w') do |f|
      f.write(YAML.dump(LucaSupport::Code.readable(obj.sort.to_h)))
    end
    obj['id']
  end
end

#delete(id, basedir = @dirname) ⇒ Object

delete file by id



235
236
237
238
# File 'lib/luca_record/io.rb', line 235

def delete(id, basedir = @dirname)
  FileUtils.rm(Pathname(abs_path(basedir)) / id2path(id))
  id
end

#dir_digest(year, month, basedir = @dirname) ⇒ Object

Calculate md5sum under specific month directory.



317
318
319
320
321
322
323
324
# File 'lib/luca_record/io.rb', line 317

def dir_digest(year, month, basedir = @dirname)
  subdir = year.to_s + LucaSupport::Code.encode_month(month)
  digest = String.new
  open_records(basedir, subdir).each do |f, path|
    digest = update_digest(digest, f.read, path[1])
  end
  digest
end

#encode_hashed_path(id, split_factor = 3) ⇒ Object

Directory separation for performance. Same as Git way.



286
287
288
289
290
291
292
293
# File 'lib/luca_record/io.rb', line 286

def encode_hashed_path(id, split_factor = 3)
  len = id.length
  if len <= split_factor
    ['', id]
  else
    [id[0, split_factor], id[split_factor, len - split_factor]]
  end
end

#find(id, basedir = @dirname) ⇒ Object

find ID based record. Support uuid and encoded date.



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/luca_record/io.rb', line 33

def find(id, basedir = @dirname)
  return enum_for(:find, id, basedir).first unless block_given?

  if id.length >= 40
    open_hashed(basedir, id) do |f|
      yield load_data(f)
    end
  elsif id.length >= 7
    parts = id.split('/')
    open_records(basedir, parts[0], parts[1]) do |f, path|
      yield load_data(f, path)
    end
  else
    raise 'specified id length is too short'
  end
end

#find_secure(id, basedir = @dirname, prefix = 's_') ⇒ Object

Merge 2 files under plain & prefixed dirs. prefixed contents will override duplicated keys. This function does not provide encryption/decryption.



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/luca_record/io.rb', line 54

def find_secure(id, basedir = @dirname, prefix = 's_')
  if id.length >= 40
    plain = open_hashed(basedir, id) do |f|
      load_data(f)
    end
    secure = begin
               open_hashed("#{prefix}#{basedir}", id) do |f|
                 load_data(f)
               end
             rescue
               # No file exists, and so on.
               {}
             end
  elsif id.length >= 7
    parts = id.split('/')
    plain = open_records(basedir, parts[0], parts[1]) do |f|
      load_data(f)
    end
    secure = open_records("#{prefix}#{basedir}", parts[0], parts[1]) do |f|
      load_data(f)
    end
  else
    raise 'specified id length is too short'
  end
  plain.merge(secure)
end

#id2path(id) ⇒ Object

Convert ID to file directory/filename path. 1st element of Array is used as directory, the others as filename. String without ‘/’ is converted as git-like structure. Normal argument is as follows:

['2020H', 'V001', 'a7b806d04a044c6dbc4ce72932867719']
  => '2020H/V001-a7b806d04a044c6dbc4ce72932867719'
'a7b806d04a044c6dbc4ce72932867719'
  => 'a7b/806d04a044c6dbc4ce72932867719'
'2020H/V001'
  => '2020H/V001'


270
271
272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/luca_record/io.rb', line 270

def id2path(id)
  if id.is_a?(Array)
    case id.length
    when 0..2
      id.join('/')
    else
      [id[0], id[1..-1].join('-')].join('/')
    end
  elsif id.include?('/')
    id
  else
    encode_hashed_path(id).join('/')
  end
end

#id_completion(phrase, label: 'name', basedir: @dirname) ⇒ Object

If multiple ID matched, return short ID and human readable label.



174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/luca_record/io.rb', line 174

def id_completion(phrase, label: 'name', basedir: @dirname)
  list = prefix_search(phrase, basedir: basedir)
  case list.length
  when 1
    list
  when 0
    raise 'No match on specified phrase'
  else
    (3..list[0].length).each do |l|
      if list.map { |id| id[0, l] }.uniq.length == list.length
        return list.map { |id| { id: id[0, l], label: find(id).dig(label) } }
      end
    end
  end
end

#latest_month(code = nil, basedir = @dirname) ⇒ Object

year, month

pair of the latest record



127
128
129
# File 'lib/luca_record/io.rb', line 127

def latest_month(code = nil, basedir = @dirname)
  LucaSupport::Code.decode_term(Dir.entries(abs_path(basedir)).max)
end

#load_config(path = nil, ext_conf: nil) ⇒ Object



326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
# File 'lib/luca_record/io.rb', line 326

def load_config(path = nil, ext_conf: nil)
  dir, file = if (filepath = Pathname(path)).file?
                [filepath.dirname, filepath]
              elsif filepath.directory?
                [filepath, filepath / 'config.yml']
              else
                nil
              end
  return {} if dir.nil?

  {}.tap do |config|
    begin
      if ! dir.join('.git').exist? \
         && (parent = dir.parent).join('.git/objects').directory? \
         && (parent_config = parent.join('config.yml')).file?
        config.merge!(YAML.safe_load(
                        parent_config.read,
                        permitted_classes: [Date]
                      ))
      end
    end
    begin
      part = YAML.safe_load(
        file.read,
        permitted_classes: [Date]
      )
      config.merge!(part) do |_k, v_self, v_new|
        if v_self.is_a? Hash
          v_self.merge(v_new)
        else
          v_new
        end
      end
    rescue Errno::ENOENT
      STDERR.puts "INFO: #{file} not found. Continue with default settings."
    end
    if ext_conf
      begin
        part = YAML.safe_load(
          (Pathname(CONST.configdir) / ext_conf).read,
          permitted_classes: [Date]
        )
        config.merge!(part) do |_k, v_self, v_new|
          if v_self.is_a? Hash
            v_self.merge(v_new)
          else
            v_new
          end
        rescue Errno::ENOENT
          STDERR.puts "WARN: #{ext_conf} not found. Extended options are not effective."
        end
      end
    end
  end
end

#load_project(path, ext_conf: nil) ⇒ Object



295
296
297
298
299
300
301
302
303
# File 'lib/luca_record/io.rb', line 295

def load_project(path, ext_conf: nil)
  CONST.set_pjdir(path)
  config = {
    'decimal_separator' => '.',
    'thousands_separator' => ','
  }.merge!(load_config(CONST.configdir, ext_conf: ext_conf))
  config['decimal_num'] ||= config['country'] == 'jp' ? 0 : 2
  CONST.set_config(config)
end

#new_record_id(basedir, date_obj) ⇒ Object



311
312
313
# File 'lib/luca_record/io.rb', line 311

def new_record_id(basedir, date_obj)
  LucaSupport::Code.encode_txid(new_record_no(basedir, date_obj))
end

#prefix_search(phrase, basedir: @dirname) ⇒ Object



190
191
192
193
194
195
# File 'lib/luca_record/io.rb', line 190

def prefix_search(phrase, basedir: @dirname)
  glob_str = phrase.length <= 3 ? "#{phrase}*/*" : "#{id2path(phrase)}*"
  Dir.chdir(abs_path(basedir)) do
    Dir.glob(glob_str).to_a.map! { |path| path.gsub!('/', '') }
  end
end

#prepare_dir!(basedir, date_obj) ⇒ Object



197
198
199
200
201
# File 'lib/luca_record/io.rb', line 197

def prepare_dir!(basedir, date_obj)
  dir_name = (Pathname(basedir) + LucaSupport::Code.encode_dirname(date_obj)).to_s
  FileUtils.mkdir_p(dir_name) unless Dir.exist?(dir_name)
  dir_name
end

#save(obj, basedir = @dirname) ⇒ Object

update file with obj



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/luca_record/io.rb', line 213

def save(obj, basedir = @dirname)
  if obj['id'].nil?
    create(obj, basedir)
  else
    validate_keys(obj)
    if obj['id'].length < 40
      parts = obj['id'].split('/')
      raise 'invalid ID' if parts.length != 2

      open_records(basedir, parts[0], parts[1], nil, 'w') do |f, path|
        f.write(YAML.dump(LucaSupport::Code.readable(obj.sort.to_h)))
      end
    else
      open_hashed(basedir, obj['id'], 'w') do |f|
        f.write(YAML.dump(LucaSupport::Code.readable(obj.sort.to_h)))
      end
    end
  end
  obj['id']
end

#search(year, month = nil, day = nil, code = nil, basedir = @dirname) ⇒ Object

search with date params & code.



106
107
108
109
110
111
112
113
# File 'lib/luca_record/io.rb', line 106

def search(year, month = nil, day = nil, code = nil, basedir = @dirname)
  return enum_for(:search, year, month, day, code, basedir) unless block_given?

  subdir = year.to_s + LucaSupport::Code.encode_month(month)
  open_records(basedir, subdir, LucaSupport::Code.encode_date(day), code) do |f, path|
    yield load_data(f, path), path
  end
end

#term(start_year, start_month, end_year, end_month, code = nil, basedir = @dirname) ⇒ Object

scan ranging data on multiple months



94
95
96
97
98
99
100
101
102
# File 'lib/luca_record/io.rb', line 94

def term(start_year, start_month, end_year, end_month, code = nil, basedir = @dirname)
  return enum_for(:term, start_year, start_month, end_year, end_month, code, basedir) unless block_given?

  LucaSupport::Code.encode_term(start_year, start_month, end_year, end_month).each do |subdir|
    open_records(basedir, subdir, nil, code) do |f, path|
      yield load_data(f, path)
    end
  end
end

#upsert(obj, basedir: @dirname) ⇒ Object

update uuid keyed record based on ‘id’ field. If not found, just create



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/luca_record/io.rb', line 155

def upsert(obj, basedir: @dirname)
  return nil if obj['id'].nil?

  validate_keys(obj)
  merged = begin
              open_hashed(basedir, obj['id'], 'r') do |f|
                load_data(f).merge(obj)
              end
            rescue
              obj
            end
  open_hashed(basedir, obj['id'], 'w') do |f|
    f.write(YAML.dump(LucaSupport::Code.readable(merged.sort.to_h)))
  end
  obj['id']
end

#valid_project?(path = CONST.pjdir) ⇒ Boolean

test if having required dirs/files under exec path



306
307
308
309
# File 'lib/luca_record/io.rb', line 306

def valid_project?(path = CONST.pjdir)
  project_dir = Pathname(path)
  FileTest.directory?( (project_dir / 'data').to_s)
end