loading
Generated 2026-05-21T17:46:04+00:00

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

5 files in total.
150 relevant lines, 150 lines covered and 0 lines missed. ( 100.0% )
24 total branches, 24 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/containable.rb 100.00 % 15 8 8 0 18.50 100.00 % 0 0 0
lib/containable/builder.rb 100.00 % 116 60 60 0 165.75 100.00 % 6 6 0
lib/containable/register.rb 100.00 % 79 45 45 0 80.56 100.00 % 10 10 0
lib/containable/resolver.rb 100.00 % 38 19 19 0 39.79 100.00 % 4 4 0
lib/containable/test.rb 100.00 % 36 18 18 0 22.56 100.00 % 4 4 0

lib/containable.rb

100.0% lines covered

100.0% branches covered

8 relevant lines. 8 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "containable/builder"
  3. 1 require "containable/register"
  4. 1 require "containable/resolver"
  5. # Main namespace.
  6. 1 module Containable
  7. 1 def self.extended descendant
  8. 71 super
  9. 71 descendant.extend Builder.new
  10. end
  11. 1 def self.[](register: Register, resolver: Resolver) = Builder.new(register:, resolver:)
  12. end

lib/containable/builder.rb

100.0% lines covered

100.0% branches covered

60 relevant lines. 60 lines covered and 0 lines missed.
6 total branches, 6 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "concurrent/hash"
  3. 1 module Containable
  4. # Provides safe registration and resolution of dependencies.
  5. 1 class Builder < Module
  6. 1 def initialize dependencies = Concurrent::Hash.new, register: Register, resolver: Resolver
  7. 219 super()
  8. 219 @dependencies = dependencies
  9. 219 @register = register.new dependencies
  10. 219 @resolver = resolver.new dependencies
  11. 3504 private_methods.grep(/\A(define)_/).sort.each { |method| __send__ method }
  12. 219 alias_method :[]=, :register
  13. 219 alias_method :[], :resolve
  14. 219 freeze
  15. end
  16. 1 def extended descendant
  17. 218 then: 3 else: 215 fail TypeError, "Only a module can be a container." if descendant.is_a? Class
  18. 215 super
  19. 1 descendant.class_eval "private_class_method :dependencies", __FILE__, __LINE__
  20. end
  21. 1 private
  22. 1 attr_reader :dependencies, :register, :resolver
  23. 1 def define_dependencies target = dependencies
  24. 558 define_method(:dependencies) { target }
  25. end
  26. 1 def define_register target = register
  27. 219 define_method :register do |key, value = nil, as: :cache, &block|
  28. 205 then: 9 else: 196 fail FrozenError, "Can't modify frozen container." if dependencies.frozen?
  29. 196 target.call key, value, as:, &block
  30. 190 self
  31. end
  32. end
  33. 1 def define_merge target: register
  34. 219 define_method :merge do |other, *keys, namespace: nil, as: :cache|
  35. 6 target.merge(other, *keys, namespace:, as:)
  36. 6 self
  37. end
  38. end
  39. 1 def define_namespace target = register
  40. 219 define_method :namespace do |name, &block|
  41. 15 target.namespace name, &block
  42. 15 self
  43. end
  44. end
  45. 1 def define_resolve target = resolver
  46. 297 define_method(:resolve) { |key| target.call key }
  47. end
  48. 1 def define_each target = dependencies
  49. 247 define_method(:each) { |&block| target.transform_values(&:first).each(&block) }
  50. end
  51. 1 def define_each_key target = dependencies
  52. 225 define_method(:each_key) { |&block| target.each_key(&block) }
  53. end
  54. 1 def define_key? target = dependencies
  55. 293 define_method(:key?) { |name| target.key? name }
  56. end
  57. 1 def define_keys target = dependencies
  58. 225 define_method(:keys) { target.keys }
  59. end
  60. 1 def define_clone
  61. 219 define_method :clone do
  62. 24 then: 3 else: 9 dup.tap { |duplicate| duplicate.freeze if dependencies.frozen? }
  63. end
  64. end
  65. 1 def define_dup target = self.class,
  66. local_register: register.class,
  67. local_resolver: resolver.class
  68. 219 define_method :dup do
  69. 24 instance = target.new dependencies.dup, register: local_register, resolver: local_resolver
  70. 24 Module.new.set_temporary_name("containable").extend instance
  71. end
  72. end
  73. 1 def define_freeze
  74. 243 define_method(:freeze) { dependencies.freeze and self }
  75. end
  76. 1 def define_frozen?
  77. 234 define_method(:frozen?) { dependencies.frozen? }
  78. end
  79. 1 def define_stub
  80. 219 define_method :stub! do |**keywords|
  81. 36 require "containable/test"
  82. 36 extend Test
  83. 36 stub(**keywords)
  84. end
  85. end
  86. 4 def define_restore = define_method(:restore) { false }
  87. end
  88. end

lib/containable/register.rb

100.0% lines covered

100.0% branches covered

45 relevant lines. 45 lines covered and 0 lines missed.
10 total branches, 10 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "concurrent/hash"
  3. 1 module Containable
  4. # :reek:TooManyInstanceVariables
  5. # Registers dependencies for future evaluation.
  6. 1 class Register
  7. 1 SEPARATOR = "."
  8. 1 DIRECTIVES = %i[cache fresh].freeze
  9. 1 def initialize dependencies = Concurrent::Hash.new, separator: SEPARATOR, directives: DIRECTIVES
  10. 237 @dependencies = dependencies
  11. 237 @separator = separator
  12. 237 @directives = directives
  13. 237 @keys = []
  14. 237 @depth = 0
  15. end
  16. 1 def call key, value = nil, as: :cache, &block
  17. 236 then: 1 else: 235 warn "Registration of value is ignored since block takes precedence." if value && block
  18. 236 namespaced_key = namespacify key
  19. 236 check_duplicate key, namespaced_key
  20. 229 check_directive as
  21. 228 dependencies[namespaced_key] = [block || value, as]
  22. 228 self
  23. end
  24. 1 alias register call
  25. 1 def merge other, *keys, namespace: nil, as: :cache
  26. 26 keys.each { call [namespace, it].compact.join("."), other[it], as: }
  27. 11 self
  28. end
  29. 1 def namespace(name, &)
  30. 36 then: 20 else: 16 keys.clear if depth.zero?
  31. 36 keys.append name
  32. 36 visit(&)
  33. 36 self
  34. end
  35. 1 protected
  36. 1 attr_reader :dependencies, :separator, :directives
  37. 1 attr_accessor :keys, :depth
  38. 1 def namespacify(key) = keys[..depth].append(key).join separator
  39. 1 def check_duplicate key, namespaced_key
  40. 236 message = "Dependency is already registered: #{key.inspect}."
  41. 236 then: 7 else: 229 fail KeyError, message if dependencies.key? namespaced_key
  42. end
  43. 1 def check_directive value
  44. 229 then: 228 else: 1 return if directives.include? value
  45. 1 fail ArgumentError,
  46. %(Invalid directive: #{value.inspect}. Use #{directives.map(&:inspect).join " or "}.)
  47. end
  48. 1 private
  49. 1 def visit &block
  50. 36 increment
  51. 36 then: 28 else: 8 instance_eval(&block) if block
  52. 36 keys.pop
  53. 36 decrement
  54. end
  55. 1 def increment = self.depth += 1
  56. 1 def decrement = self.depth -= 1
  57. end
  58. end

lib/containable/resolver.rb

100.0% lines covered

100.0% branches covered

19 relevant lines. 19 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "concurrent/hash"
  3. 1 module Containable
  4. # Resolves previously registered dependencies.
  5. 1 class Resolver
  6. 1 def initialize dependencies = Concurrent::Hash.new
  7. 227 @dependencies = dependencies
  8. end
  9. 1 def call key
  10. 87 tuple = fetch key
  11. 79 value, as = tuple
  12. 79 else: 45 then: 34 return value unless value.is_a?(Proc) && value.arity.zero?
  13. 45 process key, value, as
  14. end
  15. 1 protected
  16. 1 attr_reader :dependencies
  17. 1 def fetch key
  18. 87 dependencies.fetch key.to_s do
  19. 8 fail KeyError, "Unable to resolve dependency: #{key.inspect}."
  20. end
  21. end
  22. 1 def process key, closure, directive
  23. 45 value = closure.call
  24. 45 then: 43 else: 2 dependencies[key.to_s] = [value, directive] if directive == :cache
  25. 45 value
  26. end
  27. end
  28. end

lib/containable/test.rb

100.0% lines covered

100.0% branches covered

18 relevant lines. 18 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Containable
  3. # Allows stubbing of dependencies for testing purposes only.
  4. 1 module Test
  5. 13 def resolve(key) = stubs.fetch(key.to_s) { super }
  6. 1 alias [] resolve
  7. 1 def stub(**overrides)
  8. 68 @originals ||= dependencies.dup
  9. 68 overrides.each do |key, value|
  10. 68 normalized_key = key.to_s
  11. 68 else: 64 then: 4 fail KeyError, "Unable to stub unknown key: #{key.inspect}." unless key? normalized_key
  12. 64 stubs[normalized_key] = value
  13. end
  14. end
  15. 1 def stub!(**) = stub(**)
  16. 1 def restore
  17. 16 stubs.clear
  18. 16 then: 15 else: 1 dependencies.replace originals if originals
  19. 16 true
  20. end
  21. 1 private
  22. 1 def originals = @originals
  23. 1 def stubs = @stubs ||= {}
  24. end
  25. end