Code Coverage
 
Lines
Covered
83.90% covered (warning)
83.90%
99 / 118
1
require 'time'
1
require 'yaml/store'
1
require 'uuid'
1
require 'digest/sha1'
1
require 'pathname'
1
module TheFox
1
module Timr
# See BasicModel for more details.
1
module Model
# Basic Class
#
# Models hold data and can be stored to YAML files. Except for [Tracks](rdoc-ref:Track). Tracks are stored to a Task file.
1
class BasicModel
1
include TheFox::Timr::Error
# When calling `save_to_file`, it will only write the file if `@has_changed` is `true`.
1
attr_accessor :has_changed
# Path to file.
1
attr_accessor :file_path
1
def initialize
# id 40 chars long.
178
id = Digest::SHA1.hexdigest(UUID.new.generate)
178
@meta = {
'id' => id,
'short_id' => id[0, 6],
'created' => Time.now.utc.strftime(MODEL_DATETIME_FORMAT),
'modified' => Time.now.utc.strftime(MODEL_DATETIME_FORMAT),
}
178
@data = nil
178
@has_changed = false
178
@file_path = nil
end
# Set ID.
1
def id=(id)
54
@meta['id'] = id
54
changed
end
# Get ID.
1
def id
197
@meta['id']
end
# Get Short ID. Only 6 chars long.
1
def short_id
117
@meta['id'][0, 6]
end
# Set created Time String.
1
def created=(created)
0
@meta['created'] = created
end
# Set modified Time String.
1
def modified=(modified)
0
@meta['modified'] = modified
end
# Mark an object as changed. Only changed objects are stored to files on save_to_file().
1
def changed
315
@meta['modified'] = Time.now.utc.strftime(MODEL_DATETIME_FORMAT)
315
@has_changed = true
end
# Load an entity into the current instance.
#
# If `path` is not given `@file_path` will be taken. If `@file_path` is also not given throw ModelError exception.
1
def load_from_file(path = nil)
4
load = pre_load_from_file
4
if path.nil?
4
path = @file_path
4
if path.nil?
1
raise ModelError, 'Path cannot be nil.'
end
else
0
@file_path = path
end
3
if load
3
content = YAML::load_file(path)
3
@meta = content['meta']
3
@data = content['data']
3
@has_changed = false
end
3
post_load_from_file
end
# Hook function for subclass called before `load_from_file` payload will be executed.
1
def pre_load_from_file
4
true
end
# Hook function for subclass called after `load_from_file` payload was executed.
#
# Subclasses can access `@meta` and `@data` to write values into instance variables, or to convert data to other formats.
#
# See `pre_save_to_file`.
1
def post_load_from_file
end
# Save an entity to a YAML file.
1
def save_to_file(path = nil, force = false)
10
store = pre_save_to_file
10
if path.nil?
10
path = @file_path
10
if path.nil?
1
raise ModelError, 'Path cannot be nil.'
end
else
0
@file_path = path
end
9
if force || (store && @has_changed)
4
@meta['modified'] = Time.now.utc.strftime(MODEL_DATETIME_FORMAT)
# Create underlying directories.
4
unless path.dirname.exist?
0
path.dirname.mkpath
end
4
store = YAML::Store.new(path)
4
store.transaction do
4
store['meta'] = @meta
4
store['data'] = @data
end
4
@has_changed = false
end
9
post_save_to_file
end
# Hook function for subclass called before `save_to_file` payload will be executed.
#
# Subclasses can modify `@meta` and `@data` in this method to store more informations to the meta Hash, or to convert data to other formats that can be better written to file.
#
# For example, it's probably better to convert a floating point number to a `%.2f` formatted String and convert it back to float on `post_load_from_file`.
# See Floating Point Math <http://0.30000000000000004.com>.
1
def pre_save_to_file
3
true
end
# Hook function for subclass called after `save_to_file` payload was executed.
1
def post_save_to_file
end
# Delete the file.
1
def delete_file(path = nil)
1
path ||= @file_path
1
if path.nil?
1
raise ModelError, 'Path cannot be nil.'
end
0
path.delete
end
# All methods in this block are static.
1
class << self
1
include TheFox::Timr::Error
# Converts an [SHA1](http://ruby-doc.org/stdlib-2.4.1/libdoc/digest/rdoc/Digest/SHA1.html) Hash into a path.
#
# Function IO:
#
# ```
# 3dd50a2b50eabc84022a23ad2c06d9bb6396f978 <- input
# 3d/d50a2b50eabc84022a23ad2c06d9bb6396f978
# 3d/d50a2b50eabc84022a23ad2c06d9bb6396f978
# 3d/d5/0a2b50eabc84022a23ad2c06d9bb6396f978
# 3d/d5/0a/2b50eabc84022a23ad2c06d9bb6396f978
# 3d/d5/0a/2b50eabc84022a23ad2c06d9bb6396f978.yml <- output
# ```
1
def create_path_by_id(base_path, id)
3
if base_path.is_a?(String)
3
base_path = Pathname.new(base_path)
end
3
unless id.is_a?(String)
1
raise IdError, "ID is not a String. #{id.class} given."
end
2
if id.length <= 6
1
raise IdError, "ID is too short for creating a path. Minimum length: 7"
end
1
path_s = '%s/%s/%s/%s.yml' % [id[0, 2], id[2, 2], id[4, 2], id[6..-1]]
1
Pathname.new(path_s).expand_path(base_path)
end
# Opposite of `find_file_by_id`.
#
# Function IO:
#
# ```
# 3d/d5/0a/2b50eabc84022a23ad2c06d9bb6396f978.yml <- input
# 3dd50a2b50eabc84022a23ad2c06d9bb6396f978 <- output
# ```
1
def get_id_from_path(base_path, path)
1
path.relative_path_from(base_path).to_s.gsub('/', '')[0..-5]
end
# Opposite of `get_id_from_path`.
1
def find_file_by_id(base_path, id)
3
if base_path.is_a?(String)
3
base_path = Pathname.new(base_path)
end
3
unless id.is_a?(String)
1
raise IdError, "ID '#{id}' is not a String. #{id.class} given."
end
2
if id.length < 4
1
raise IdError, "ID '#{id}' is too short for find. Minimum length: 4"
end
1
if id.length == 40
0
path = create_path_by_id(base_path, id)
else
# 12/34
1
search_path = '%s/%s' % [id[0, 2], id[2, 2]]
1
if id.length >= 5
# 12/34/5
0
search_path << '/' << id[4]
0
if id.length >= 6
# 12/34/56
0
search_path << id[5]
0
if id.length >= 7
# 12/34/56/xxxxxx
0
search_path << '/' << id[6..-1]
end
end
end
1
search_path << '*'
1
path = nil
1
base_path.find.each do |file|
# Filter all directories.
4
unless file.file?
3
next
end
# Filter all non-yaml files.
1
unless file.basename.fnmatch('*.yml')
1
next
end
0
rel_path = file.relative_path_from(base_path)
0
unless rel_path.fnmatch(search_path)
0
next
end
0
if path
0
raise ModelError.new(id), "ID '#{id}' is not a unique identifier."
else
0
path = file
# Do not break the loop here.
# Iterate all keys to make sure the ID is unique.
end
end
end
1
if path && path.exist?
0
return path
end
1
raise ModelError, "Could not find a file for ID '#{id}' at #{base_path}."
end
end
end # class Model
end # module Model
end # module Timr
end #module TheFox