loading
Generated 2025-10-08T23:57:55+00:00

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

5 files in total.
136 relevant lines, 136 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 % 25 13 13 0 13.69 100.00 % 0 0 0
lib/containable/builder.rb 100.00 % 93 47 47 0 143.00 100.00 % 6 6 0
lib/containable/register.rb 100.00 % 70 39 39 0 61.72 100.00 % 10 10 0
lib/containable/resolver.rb 100.00 % 38 19 19 0 33.05 100.00 % 4 4 0
lib/containable/test.rb 100.00 % 36 18 18 0 9.89 100.00 % 4 4 0

lib/containable.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 "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. 1 def stub!(**)
  13. 12 require "containable/test"
  14. 12 extend Test
  15. 12 stub(**)
  16. end
  17. 1 def restore = false
  18. end

lib/containable/builder.rb

100.0% lines covered

100.0% branches covered

47 relevant lines. 47 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. 175 super()
  8. 175 @dependencies = dependencies
  9. 175 @register = register.new dependencies
  10. 175 @resolver = resolver.new dependencies
  11. 2275 private_methods.grep(/\A(define)_/).sort.each { |method| __send__ method }
  12. 175 alias_method :[]=, :register
  13. 175 alias_method :[], :resolve
  14. 175 freeze
  15. end
  16. 1 def extended descendant
  17. 174 then: 3 else: 171 fail TypeError, "Only a module can be a container." if descendant.is_a? Class
  18. 171 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. 413 define_method(:dependencies) { target }
  25. end
  26. 1 def define_register target = register
  27. 175 define_method :register do |key, value = nil, as: :cache, &block|
  28. 142 then: 9 else: 133 fail FrozenError, "Can't modify frozen container." if dependencies.frozen?
  29. 133 target.call key, value, as:, &block
  30. end
  31. end
  32. 1 def define_namespace target = register
  33. 187 define_method(:namespace) { |name, &block| target.namespace name, &block }
  34. end
  35. 1 def define_resolve target = resolver
  36. 238 define_method(:resolve) { |key| target.call key }
  37. end
  38. 1 def define_each target = dependencies
  39. 198 define_method(:each) { |&block| target.transform_values(&:first).each(&block) }
  40. end
  41. 1 def define_each_key target = dependencies
  42. 181 define_method(:each_key) { |&block| target.each_key(&block) }
  43. end
  44. 1 def define_key? target = dependencies
  45. 209 define_method(:key?) { |name| target.key? name }
  46. end
  47. 1 def define_keys target = dependencies
  48. 181 define_method(:keys) { target.keys }
  49. end
  50. 1 def define_clone
  51. 175 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. 175 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. 199 define_method(:freeze) { dependencies.freeze and self }
  65. end
  66. 1 def define_frozen?
  67. 190 define_method(:frozen?) { dependencies.frozen? }
  68. end
  69. end
  70. end

lib/containable/register.rb

100.0% lines covered

100.0% branches covered

39 relevant lines. 39 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. 186 @dependencies = dependencies
  11. 186 @separator = separator
  12. 186 @directives = directives
  13. 186 @keys = []
  14. 186 @depth = 0
  15. end
  16. 1 def call key, value = nil, as: :cache, &block
  17. 157 then: 1 else: 156 warn "Registration of value is ignored since block takes precedence." if value && block
  18. 157 namespaced_key = namespacify key
  19. 157 check_duplicate key, namespaced_key
  20. 150 check_directive as
  21. 149 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 private
  30. 1 attr_reader :dependencies, :separator, :directives
  31. 1 attr_accessor :keys, :depth
  32. 1 def check_duplicate key, namespaced_key
  33. 157 message = "Dependency is already registered: #{key.inspect}."
  34. 157 then: 7 else: 150 fail KeyError, message if dependencies.key? namespaced_key
  35. end
  36. 1 def check_directive value
  37. 150 then: 149 else: 1 return if directives.include? value
  38. 1 fail ArgumentError,
  39. %(Invalid directive: #{value.inspect}. Use #{directives.map(&:inspect).join " or "}.)
  40. end
  41. 1 def visit &block
  42. 32 increment
  43. 32 then: 28 else: 4 instance_eval(&block) if block
  44. 32 keys.pop
  45. 32 decrement
  46. end
  47. 1 def increment = self.depth += 1
  48. 1 def decrement = self.depth -= 1
  49. 1 def namespacify(key) = keys[..depth].append(key).join separator
  50. end
  51. 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. 183 @dependencies = dependencies
  8. end
  9. 1 def call key
  10. 72 tuple = fetch key
  11. 64 value, as = tuple
  12. 64 else: 39 then: 25 return value unless value.is_a?(Proc) && value.arity.zero?
  13. 39 process key, value, as
  14. end
  15. 1 private
  16. 1 attr_reader :dependencies
  17. 1 def fetch key
  18. 72 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. 39 value = closure.call
  24. 39 then: 37 else: 2 dependencies[key.to_s] = [value, directive] if directive == :cache
  25. 39 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. 7 def resolve(key) = stubs.fetch(key.to_s) { super }
  6. 1 alias [] resolve
  7. 1 def stub(**overrides)
  8. 28 @originals ||= dependencies.dup
  9. 28 overrides.each do |key, value|
  10. 28 normalized_key = key.to_s
  11. 28 else: 26 then: 2 fail KeyError, "Unable to stub unknown key: #{key.inspect}." unless key? normalized_key
  12. 26 stubs[normalized_key] = value
  13. end
  14. end
  15. 1 def stub!(**) = stub(**)
  16. 1 def restore
  17. 8 stubs.clear
  18. 8 then: 7 else: 1 dependencies.replace originals if originals
  19. 8 true
  20. end
  21. 1 private
  22. 1 def originals = @originals
  23. 1 def stubs = @stubs ||= {}
  24. end
  25. end