loading
Generated 2025-10-08T23:59:33+00:00

All Files ( 100.0% covered at 7.28 hits/line )

8 files in total.
137 relevant lines, 137 lines covered and 0 lines missed. ( 100.0% )
13 total branches, 13 branches covered and 0 branches missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches
lib/lode.rb 100.00 % 21 11 11 0 1.45 100.00 % 0 0 0
lib/lode/client.rb 100.00 % 39 20 20 0 4.00 100.00 % 2 2 0
lib/lode/configuration.rb 100.00 % 30 13 13 0 3.69 100.00 % 0 0 0
lib/lode/refines/persistent_store.rb 100.00 % 31 17 17 0 3.41 100.00 % 5 5 0
lib/lode/setting.rb 100.00 % 10 4 4 0 16.00 100.00 % 0 0 0
lib/lode/tables/abstract.rb 100.00 % 85 45 45 0 13.93 100.00 % 4 4 0
lib/lode/tables/hash.rb 100.00 % 23 11 11 0 3.64 100.00 % 0 0 0
lib/lode/tables/value.rb 100.00 % 33 16 16 0 4.06 100.00 % 2 2 0

lib/lode.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "zeitwerk"
  3. 1 Zeitwerk::Loader.new.then do |loader|
  4. 1 loader.tag = File.basename __FILE__, ".rb"
  5. 1 loader.push_dir __dir__
  6. 1 loader.setup
  7. end
  8. # Main namespace.
  9. 1 module Lode
  10. 1 PRIMARY_KEY = :id
  11. 1 MODES = %i[default thread file max].freeze
  12. 1 def self.loader registry = Zeitwerk::Registry
  13. 6 @loader ||= registry.loaders.each.find { |loader| loader.tag == File.basename(__FILE__, ".rb") }
  14. end
  15. 1 def self.new(...) = Client.new(...)
  16. end

lib/lode/client.rb

100.0% lines covered

100.0% branches covered

20 relevant lines. 20 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "forwardable"
  3. 1 require "refinements/pathname"
  4. 1 module Lode
  5. # Provides an enhanced PStore-based client.
  6. 1 class Client
  7. 1 extend Forwardable
  8. 1 using Refinements::Pathname
  9. 1 attr_reader :path, :store
  10. 1 delegate %i[register registry] => :configuration
  11. 1 def initialize path, configuration: Configuration.new
  12. 13 then: 1 else: 12 yield configuration if block_given?
  13. 13 @path = Pathname(path).make_ancestors
  14. 13 @configuration = configuration
  15. 13 @store = configuration.store_for path
  16. end
  17. 1 def read(key, &) = transact(__method__, key, &)
  18. 1 def write(key, &) = transact(:commit, key, &)
  19. 1 private
  20. 1 attr_reader :configuration
  21. 1 def transact(mode, key, &)
  22. 7 store.transaction mode == :read do
  23. 7 configuration.table_for(store, key).instance_eval(&)
  24. end
  25. end
  26. end
  27. end

lib/lode/configuration.rb

100.0% lines covered

100.0% branches covered

13 relevant lines. 13 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "pstore"
  3. 1 module Lode
  4. # Models the default configuration.
  5. 1 Configuration = Struct.new :store, :mode, :table, :primary_key, :registry do
  6. 1 using Refines::PersistentStore
  7. 1 using Refinements::Array
  8. 1 def initialize store: PStore,
  9. mode: :default,
  10. table: Tables::Hash,
  11. primary_key: PRIMARY_KEY,
  12. registry: {}
  13. 20 super
  14. end
  15. 1 def store_for(path) = store.with(path, mode:)
  16. 1 def table_for store, key, setting: Setting.new
  17. 9 table.new store, key, setting: registry.fetch(key, setting)
  18. end
  19. 1 def register key, model: Hash, primary_key: PRIMARY_KEY
  20. 5 registry[key] = Setting[model:, primary_key:]
  21. 5 self
  22. end
  23. end
  24. end

lib/lode/refines/persistent_store.rb

100.0% lines covered

100.0% branches covered

17 relevant lines. 17 lines covered and 0 lines missed.
5 total branches, 5 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "pstore"
  3. 1 require "refinements/array"
  4. 1 module Lode
  5. 1 module Refines
  6. # Refines and enhances PStore functionality.
  7. 1 module PersistentStore
  8. 1 using Refinements::Array
  9. 1 refine PStore.singleton_class do
  10. 1 def with path, mode: :default
  11. 22 when: 15 case mode
  12. 15 when: 3 when :default then new path
  13. 3 when: 2 when :thread then new path, true
  14. 4 when: 1 when :file then new(path).tap { |instance| instance.ultra_safe = true }
  15. 2 else: 1 when :max then new(path, true).tap { |instance| instance.ultra_safe = true }
  16. 1 else fail PStore::Error, %(Invalid mode. Use: #{MODES.to_usage "or"}.)
  17. end
  18. end
  19. end
  20. 1 refine PStore do
  21. 1 def thread_safe? = @thread_safe
  22. 1 def file_safe? = @ultra_safe
  23. end
  24. end
  25. end
  26. end

lib/lode/setting.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Lode
  3. # Defines a configuration setting.
  4. 1 Setting = Data.define :model, :primary_key do
  5. 1 def initialize model: Hash, primary_key: PRIMARY_KEY
  6. 61 super
  7. end
  8. end
  9. end

lib/lode/tables/abstract.rb

100.0% lines covered

100.0% branches covered

45 relevant lines. 45 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 require "refinements/array"
  4. 1 module Lode
  5. 1 module Tables
  6. # Provides an abstract table for subclassing.
  7. 1 class Abstract
  8. 1 include Dry::Monads[:result]
  9. 1 using Refinements::Array
  10. 1 def initialize store, key, setting: Setting.new
  11. 51 @store = store
  12. 51 @key = key
  13. 51 @setting = setting
  14. 51 @records = store.fetch key, []
  15. end
  16. 1 def primary_key = setting.primary_key
  17. 1 def all = Success records.dup.freeze
  18. 1 def find id, key: primary_key
  19. 84 records.find { |record| primary_id(record, key:) == id }
  20. .then do |record|
  21. 63 then: 19 else: 44 return Success record if record
  22. 44 Failure "Unable to find #{key}: #{id.inspect}."
  23. end
  24. end
  25. 1 def create record, key: primary_key
  26. 13 id = primary_id(record, key:)
  27. 14 find(id, key:).bind { Failure "Record exists for #{key}: #{id}." }
  28. .or do |error|
  29. 12 then: 10 else: 2 return append record if error.include? "Unable to find"
  30. 2 Failure error
  31. end
  32. end
  33. 1 def update change, key: primary_key
  34. 9 id = primary_id(change, key:)
  35. 12 find(id, key:).bind { |existing| revise existing, change }
  36. end
  37. 1 def upsert record, key: primary_key
  38. 1 fail NoMethodError,
  39. "`#{self.class}##{__method__} #{method(__method__).parameters}` must be implemented."
  40. end
  41. 1 def delete id, key: primary_key
  42. 10 find(id, key:).fmap do |record|
  43. 5 records.delete record
  44. 5 store[key] = records
  45. 5 record
  46. end
  47. end
  48. 1 protected
  49. 1 attr_reader :store, :key, :setting, :records
  50. 1 def primary_id record, key: primary_key
  51. 2 fail NoMethodError,
  52. "`#{self.class}##{__method__} #{method(__method__).parameters}` must be implemented."
  53. end
  54. 1 def revise existing, change
  55. 9 records.supplant existing, change
  56. 9 store[key] = records
  57. 9 Success change
  58. end
  59. 1 def append record
  60. 32 records.append record
  61. 32 store[key] = records
  62. 31 Success record
  63. end
  64. end
  65. end
  66. end

lib/lode/tables/hash.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 module Lode
  4. 1 module Tables
  5. # Provides an array-based table for hash objects.
  6. 1 class Hash < Abstract
  7. 1 include Dry::Monads[:result]
  8. 1 def upsert change, key: primary_key
  9. 16 find(change[key], key:).either(
  10. 3 -> existing { revise existing, change },
  11. 13 proc { append change }
  12. )
  13. end
  14. 1 protected
  15. 1 def primary_id(record, key: primary_key) = record[key]
  16. end
  17. end
  18. end

lib/lode/tables/value.rb

100.0% lines covered

100.0% branches covered

16 relevant lines. 16 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 module Lode
  4. 1 module Tables
  5. # Provides an array-based table for value objects.
  6. 1 class Value < Abstract
  7. 1 include Dry::Monads[:result]
  8. 1 def upsert change, key: primary_key
  9. 11 record = record_for change
  10. 11 find(primary_id(record, key:)).either(
  11. 2 -> existing { revise existing, record },
  12. 9 proc { append record }
  13. )
  14. end
  15. 1 protected
  16. 1 def primary_id(record, key: primary_key) = record.public_send(key)
  17. 1 private
  18. # :reek:FeatureEnvy
  19. 1 def record_for change
  20. 11 model = setting.model
  21. 11 then: 10 else: 1 change.is_a?(model) ? change : model[**change.to_h]
  22. end
  23. end
  24. end
  25. end