loading
Generated 2026-05-12T00:05:24+00:00

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

5 files in total.
138 relevant lines, 138 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 17.50 100.00 % 0 0 0
lib/containable/builder.rb 100.00 % 105 53 53 0 161.23 100.00 % 6 6 0
lib/containable/register.rb 100.00 % 72 40 40 0 73.05 100.00 % 10 10 0
lib/containable/resolver.rb 100.00 % 38 19 19 0 36.95 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. 67 super
  9. 67 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

53 relevant lines. 53 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. 201 super()
  8. 201 @dependencies = dependencies
  9. 201 @register = register.new dependencies
  10. 201 @resolver = resolver.new dependencies
  11. 3015 private_methods.grep(/\A(define)_/).sort.each { |method| __send__ method }
  12. 201 alias_method :[]=, :register
  13. 201 alias_method :[], :resolve
  14. 201 freeze
  15. end
  16. 1 def extended descendant
  17. 200 then: 3 else: 197 fail TypeError, "Only a module can be a container." if descendant.is_a? Class
  18. 197 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. 519 define_method(:dependencies) { target }
  25. end
  26. 1 def define_register target = register
  27. 201 define_method :register do |key, value = nil, as: :cache, &block|
  28. 190 then: 9 else: 181 fail FrozenError, "Can't modify frozen container." if dependencies.frozen?
  29. 181 target.call key, value, as:, &block
  30. end
  31. end
  32. 1 def define_namespace target = register
  33. 213 define_method(:namespace) { |name, &block| target.namespace name, &block }
  34. end
  35. 1 def define_resolve target = resolver
  36. 270 define_method(:resolve) { |key| target.call key }
  37. end
  38. 1 def define_each target = dependencies
  39. 226 define_method(:each) { |&block| target.transform_values(&:first).each(&block) }
  40. end
  41. 1 def define_each_key target = dependencies
  42. 207 define_method(:each_key) { |&block| target.each_key(&block) }
  43. end
  44. 1 def define_key? target = dependencies
  45. 275 define_method(:key?) { |name| target.key? name }
  46. end
  47. 1 def define_keys target = dependencies
  48. 207 define_method(:keys) { target.keys }
  49. end
  50. 1 def define_clone
  51. 201 define_method :clone do
  52. 24 then: 3 else: 9 dup.tap { |duplicate| duplicate.freeze if dependencies.frozen? }
  53. end
  54. end
  55. 1 def define_dup target = self.class,
  56. local_register: register.class,
  57. local_resolver: resolver.class
  58. 201 define_method :dup do
  59. 18 instance = target.new dependencies.dup, register: local_register, resolver: local_resolver
  60. 18 Module.new.set_temporary_name("containable").extend instance
  61. end
  62. end
  63. 1 def define_freeze
  64. 225 define_method(:freeze) { dependencies.freeze and self }
  65. end
  66. 1 def define_frozen?
  67. 216 define_method(:frozen?) { dependencies.frozen? }
  68. end
  69. 1 def define_stub
  70. 201 define_method :stub! do |**keywords|
  71. 36 require "containable/test"
  72. 36 extend Test
  73. 36 stub(**keywords)
  74. end
  75. end
  76. 4 def define_restore = define_method(:restore) { false }
  77. end
  78. end

lib/containable/register.rb

100.0% lines covered

100.0% branches covered

40 relevant lines. 40 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. 212 @dependencies = dependencies
  11. 212 @separator = separator
  12. 212 @directives = directives
  13. 212 @keys = []
  14. 212 @depth = 0
  15. end
  16. 1 def call key, value = nil, as: :cache, &block
  17. 205 then: 1 else: 204 warn "Registration of value is ignored since block takes precedence." if value && block
  18. 205 namespaced_key = namespacify key
  19. 205 check_duplicate key, namespaced_key
  20. 198 check_directive as
  21. 197 dependencies[namespaced_key] = [block || value, as]
  22. end
  23. 1 alias register call
  24. 1 def namespace(name, &)
  25. 32 then: 16 else: 16 keys.clear if depth.zero?
  26. 32 keys.append name
  27. 32 visit(&)
  28. end
  29. 1 protected
  30. 1 attr_reader :dependencies, :separator, :directives
  31. 1 attr_accessor :keys, :depth
  32. 1 def namespacify(key) = keys[..depth].append(key).join separator
  33. 1 def check_duplicate key, namespaced_key
  34. 205 message = "Dependency is already registered: #{key.inspect}."
  35. 205 then: 7 else: 198 fail KeyError, message if dependencies.key? namespaced_key
  36. end
  37. 1 def check_directive value
  38. 198 then: 197 else: 1 return if directives.include? value
  39. 1 fail ArgumentError,
  40. %(Invalid directive: #{value.inspect}. Use #{directives.map(&:inspect).join " or "}.)
  41. end
  42. 1 private
  43. 1 def visit &block
  44. 32 increment
  45. 32 then: 28 else: 4 instance_eval(&block) if block
  46. 32 keys.pop
  47. 32 decrement
  48. end
  49. 1 def increment = self.depth += 1
  50. 1 def decrement = self.depth -= 1
  51. end
  52. 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. 209 @dependencies = dependencies
  8. end
  9. 1 def call key
  10. 78 tuple = fetch key
  11. 70 value, as = tuple
  12. 70 else: 45 then: 25 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. 78 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