module LucaRecord::IO::ClassMethods

Path Utilities

↑ top

Public Instance Methods

dir_digest(year, month, basedir = @dirname) click to toggle source

Calculate md5sum under specific month directory.

# File lib/luca_record/io.rb, line 249
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) click to toggle source

Directory separation for performance. Same as Git way.

# File lib/luca_record/io.rb, line 228
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
id2path(id) click to toggle source

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'
# File lib/luca_record/io.rb, line 212
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
new_record_id(basedir, date_obj) click to toggle source
# File lib/luca_record/io.rb, line 243
def new_record_id(basedir, date_obj)
  LucaSupport::Code.encode_txid(new_record_no(basedir, date_obj))
end
valid_project?(path = LucaSupport::PJDIR) click to toggle source

test if having required dirs/files under exec path

# File lib/luca_record/io.rb, line 238
def valid_project?(path = LucaSupport::PJDIR)
  project_dir = Pathname(path)
  FileTest.file?((project_dir + 'config.yml').to_s) and FileTest.directory?( (project_dir + 'data').to_s)
end

Private Instance Methods

abs_path(base_dir) click to toggle source

TODO: replace with data_dir method

# File lib/luca_record/io.rb, line 374
def abs_path(base_dir)
  Pathname(LucaSupport::PJDIR) / 'data' / base_dir
end
create_record(obj, date_obj, codes = nil, basedir = @dirname) { |f| ... } click to toggle source

define new transaction ID & write data at once ID format is like '2020H/A001', which means record no.1 of 2020/10/10. Any data format can be written with block.

# File lib/luca_record/io.rb, line 264
def create_record(obj, date_obj, codes = nil, basedir = @dirname)
  FileUtils.mkdir_p(abs_path(basedir)) unless Dir.exist?(abs_path(basedir))
  subdir = "#{date_obj.year}#{LucaSupport::Code.encode_month(date_obj)}"
  filename = LucaSupport::Code.encode_date(date_obj) + new_record_id(basedir, date_obj)
  obj['id'] = "#{subdir}/#{filename}" if obj.is_a? Hash
  filename += '-' + codes.join('-') if codes
  Dir.chdir(abs_path(basedir)) do
    FileUtils.mkdir_p(subdir) unless Dir.exist?(subdir)
      File.open(Pathname(subdir) / filename, 'w') do |f|
        if block_given?
          yield(f)
        else
          f.write(YAML.dump(LucaSupport::Code.readable(obj.sort.to_h)))
        end
      end
  end
  "#{subdir}/#{filename}"
end
load_data(io, path = nil) click to toggle source

Decode basic format. If specific decode is needed, override this method in each class.

# File lib/luca_record/io.rb, line 352
def load_data(io, path = nil)
  if @record_type
    case @record_type
    when 'json'
      # TODO: implement JSON parse
    end
  else
    LucaSupport::Code.decimalize(YAML.load(io.read)).tap { |obj| validate_keys(obj) }
  end
end
new_record_no(basedir, date_obj) click to toggle source

AUTO INCREMENT

# File lib/luca_record/io.rb, line 392
def new_record_no(basedir, date_obj)
  raise 'No target dir exists.' unless Dir.exist?(abs_path(basedir))

  dir_name = (Pathname(abs_path(basedir)) / LucaSupport::Code.encode_dirname(date_obj)).to_s
  return 1 unless Dir.exist?(dir_name)

  Dir.chdir(dir_name) do
    last_file = Dir.glob("#{LucaSupport::Code.encode_date(date_obj)}*").max
    return 1 if last_file.nil?

    return LucaSupport::Code.decode_txid(last_file[1, 3]) + 1
  end
end
open_all(basedir, mode = 'r') { |f| ... } click to toggle source

scan through all files

# File lib/luca_record/io.rb, line 329
def open_all(basedir, mode = 'r')
  return enum_for(:open_all, basedir, mode) unless block_given?

  dirpath = Pathname(abs_path(basedir)) / '*' / '*'
  Dir.glob(dirpath.to_s).each do |filename|
    File.open(filename, mode) { |f| yield f }
  end
end
open_hashed(basedir, id, mode = 'r') { |f| ... } click to toggle source

git object like structure

# File lib/luca_record/io.rb, line 318
def open_hashed(basedir, id, mode = 'r')
  return enum_for(:open_hashed, basedir, id, mode) unless block_given?

  subdir, filename = encode_hashed_path(id)
  dirpath = Pathname(abs_path(basedir)) + subdir
  FileUtils.mkdir_p(dirpath.to_s) if mode != 'r'
  File.open((dirpath + filename).to_s, mode) { |f| yield f }
end
open_records(basedir, subdir, filename = nil, code = nil, mode = 'r') { |f, id_set| ... } click to toggle source

open records with 'basedir/month/date-code' path structure. Glob pattern can be specified like folloing examples.

'2020': All month of 2020
'2020[FG]': June & July of 2020

Block will receive code fragments as 2nd parameter. Array format is as bellows:

  1. encoded month

  2. encoded day + record number of the day

  3. codes. More than 3 are all code set except first 2 parameters.

# File lib/luca_record/io.rb, line 293
def open_records(basedir, subdir, filename = nil, code = nil, mode = 'r')
  return enum_for(:open_records, basedir, subdir, filename, code, mode) unless block_given?

  file_pattern = filename.nil? ? '*' : "#{filename}*"
  Dir.chdir(abs_path(basedir)) do
    FileUtils.mkdir_p(subdir) if mode == 'w' && !Dir.exist?(subdir)
    Dir.glob("#{subdir}*/#{file_pattern}").sort.each do |subpath|
      next if skip_on_unmatch_code(subpath, code)

      id_set = subpath.split('/').map { |str| str.split('-') }.flatten
      File.open(subpath, mode) { |f| yield(f, id_set) }
    end
  end
end
scan_terms(query = nil, base_dir = @dirname) click to toggle source

parse data dir and respond existing months

# File lib/luca_record/io.rb, line 340
def scan_terms(query = nil, base_dir = @dirname)
  pattern = query.nil? ? "*" : "#{query}*"
  Dir.chdir(abs_path(base_dir)) do
    Dir.glob(pattern).select { |dir|
      FileTest.directory?(dir) && /^[0-9]/.match(dir)
    }.sort.map { |str| decode_term(str) }
  end
end
skip_on_unmatch_code(subpath, code = nil) click to toggle source

True when file doesn't have record on code. False when file may have one. If filename doesn't record codes, always return false, so later check is required. This is partial optimization.

# File lib/luca_record/io.rb, line 383
def skip_on_unmatch_code(subpath, code = nil)
  # p filename.split('-')[1..-1]
  filename = subpath.split('/').last
  return false if code.nil? || filename.length <= 4

  filename.split('-')[1..-1].select { |fragment| /^#{code}/.match(fragment) }.empty?
end
update_digest(digest, str, filename = nil) click to toggle source

Calculate md5sum with original digest, file content and filename(optional).

# File lib/luca_record/io.rb, line 310
def update_digest(digest, str, filename = nil)
  str = filename.nil? ? str : filename + str
  content = Digest::MD5.new.update(str).hexdigest
  Digest::MD5.new.update(digest + content).hexdigest
end
validate_keys(obj) click to toggle source
# File lib/luca_record/io.rb, line 363
def validate_keys(obj)
  return nil unless @required

  keys = obj.keys
  [].tap do |errors|
    @required.each { |r| errors << r unless keys.include?(r) }
    raise "Missing keys: #{errors.join(' ')}" unless errors.empty?
  end
end

Query Methods

↑ top

Public Instance Methods

all(basedir = @dirname) { |load_data(f)| ... } click to toggle source

retrieve all data

# File lib/luca_record/io.rb, line 85
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) { |data, path| ... } click to toggle source

search date based record.

  • data hash

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

# File lib/luca_record/io.rb, line 54
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
find(id, basedir = @dirname) { |load_data(f)| ... } click to toggle source

find ID based record. Support uuid and encoded date.

# File lib/luca_record/io.rb, line 32
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
term(start_year, start_month, end_year, end_month, code = nil, basedir = @dirname) { |load_data(f, path)| ... } click to toggle source

scan ranging data on multiple months

# File lib/luca_record/io.rb, line 62
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

Write Methods

↑ top

Public Instance Methods

add_status!(id, status, basedir = @dirname) click to toggle source
# File lib/luca_record/io.rb, line 145
def add_status!(id, status, basedir = @dirname)
  path = abs_path(basedir) / id2path(id)
  origin = YAML.load_file(path, **{})
  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
change_codes(id, new_codes, basedir = @dirname) click to toggle source

change filename with new code set

# File lib/luca_record/io.rb, line 184
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) click to toggle source

create record both of uuid/date identified.

# File lib/luca_record/io.rb, line 101
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) click to toggle source

delete file by id

# File lib/luca_record/io.rb, line 177
def delete(id, basedir = @dirname)
  FileUtils.rm(Pathname(abs_path(basedir)) / id2path(id))
  id
end
id_completion(phrase, label: 'name', basedir: @dirname) click to toggle source

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

# File lib/luca_record/io.rb, line 116
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
prepare_dir!(basedir, date_obj) click to toggle source
# File lib/luca_record/io.rb, line 139
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) click to toggle source

update file with obj

# File lib/luca_record/io.rb, line 155
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