fmt 0.1.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +170 -87
  3. data/lib/fmt/boot.rb +50 -0
  4. data/lib/fmt/lru_cache.rb +181 -0
  5. data/lib/fmt/mixins/matchable.rb +26 -0
  6. data/lib/fmt/models/arguments.rb +194 -0
  7. data/lib/fmt/models/embed.rb +48 -0
  8. data/lib/fmt/models/macro.rb +58 -0
  9. data/lib/fmt/models/model.rb +66 -0
  10. data/lib/fmt/models/pipeline.rb +47 -0
  11. data/lib/fmt/models/template.rb +55 -0
  12. data/lib/fmt/node.rb +128 -0
  13. data/lib/fmt/parsers/arguments_parser.rb +43 -0
  14. data/lib/fmt/parsers/embed_parser.rb +54 -0
  15. data/lib/fmt/parsers/macro_parser.rb +113 -0
  16. data/lib/fmt/parsers/parser.rb +56 -0
  17. data/lib/fmt/parsers/pipeline_parser.rb +41 -0
  18. data/lib/fmt/parsers/template_parser.rb +125 -0
  19. data/lib/fmt/refinements/kernel_refinement.rb +38 -0
  20. data/lib/fmt/registries/native_registry.rb +66 -0
  21. data/lib/fmt/registries/rainbow_registry.rb +36 -0
  22. data/lib/fmt/registries/registry.rb +127 -0
  23. data/lib/fmt/renderer.rb +132 -0
  24. data/lib/fmt/sigils.rb +23 -0
  25. data/lib/fmt/token.rb +126 -0
  26. data/lib/fmt/tokenizer.rb +96 -0
  27. data/lib/fmt/version.rb +3 -1
  28. data/lib/fmt.rb +51 -11
  29. data/sig/generated/fmt/boot.rbs +2 -0
  30. data/sig/generated/fmt/lru_cache.rbs +122 -0
  31. data/sig/generated/fmt/mixins/matchable.rbs +18 -0
  32. data/sig/generated/fmt/models/arguments.rbs +115 -0
  33. data/sig/generated/fmt/models/embed.rbs +34 -0
  34. data/sig/generated/fmt/models/macro.rbs +37 -0
  35. data/sig/generated/fmt/models/model.rbs +45 -0
  36. data/sig/generated/fmt/models/pipeline.rbs +31 -0
  37. data/sig/generated/fmt/models/template.rbs +33 -0
  38. data/sig/generated/fmt/node.rbs +64 -0
  39. data/sig/generated/fmt/parsers/arguments_parser.rbs +25 -0
  40. data/sig/generated/fmt/parsers/embed_parser.rbs +36 -0
  41. data/sig/generated/fmt/parsers/macro_parser.rbs +60 -0
  42. data/sig/generated/fmt/parsers/parser.rbs +44 -0
  43. data/sig/generated/fmt/parsers/pipeline_parser.rbs +25 -0
  44. data/sig/generated/fmt/parsers/template_parser.rbs +50 -0
  45. data/sig/generated/fmt/refinements/kernel_refinement.rbs +23 -0
  46. data/sig/generated/fmt/registries/native_registry.rbs +19 -0
  47. data/sig/generated/fmt/registries/rainbow_registry.rbs +11 -0
  48. data/sig/generated/fmt/registries/registry.rbs +69 -0
  49. data/sig/generated/fmt/renderer.rbs +70 -0
  50. data/sig/generated/fmt/sigils.rbs +30 -0
  51. data/sig/generated/fmt/token.rbs +77 -0
  52. data/sig/generated/fmt/tokenizer.rbs +51 -0
  53. data/sig/generated/fmt/version.rbs +5 -0
  54. data/sig/generated/fmt.rbs +41 -0
  55. metadata +126 -16
  56. data/lib/fmt/embed.rb +0 -19
  57. data/lib/fmt/filter.rb +0 -32
  58. data/lib/fmt/filters.rb +0 -76
  59. data/lib/fmt/formatter.rb +0 -50
  60. data/lib/fmt/scanners/base_scanner.rb +0 -41
  61. data/lib/fmt/scanners/embed_scanner.rb +0 -56
  62. data/lib/fmt/scanners/filter_scanner.rb +0 -31
  63. data/lib/fmt/scanners/key_scanner.rb +0 -15
  64. data/lib/fmt/scanners.rb +0 -3
  65. data/lib/fmt/transformer.rb +0 -63
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rbs_inline: enabled
4
+
5
+ module Fmt
6
+ # Parses a macro from a string and builds an AST (Abstract Syntax Tree)
7
+ class MacroParser < Parser
8
+ # Constructor
9
+ # @rbs urtext: String -- original source code
10
+ def initialize(urtext = "")
11
+ @urtext = urtext.to_s
12
+ end
13
+
14
+ attr_reader :urtext # : String -- original source code
15
+
16
+ # Parses the urtext (original source code)
17
+ # @rbs return: Node -- AST (Abstract Syntax Tree)
18
+ def parse
19
+ cache(urtext) { super }
20
+ end
21
+
22
+ protected
23
+
24
+ # Extracts components for building the AST (Abstract Syntax Tree)
25
+ # @rbs return: Hash[Symbol, Object] -- extracted components
26
+ def extract
27
+ code = urtext
28
+ code = "#{Sigils::FORMAT_METHOD}('#{urtext}')" if native_format_string?(urtext)
29
+
30
+ tokens = tokenize(code)
31
+ method = tokens.find(&:method_name?)&.value&.to_sym
32
+
33
+ arguments_tokens = case arguments?(tokens)
34
+ in false then []
35
+ else
36
+ arguments_start = tokens.index(tokens.find(&:arguments_start?)).to_i
37
+ arguments_finish = tokens.index(tokens.find(&:arguments_finish?)).to_i
38
+ tokens[arguments_start..arguments_finish]
39
+ end
40
+
41
+ {method: method, arguments_tokens: arguments_tokens}
42
+ end
43
+
44
+ # Transforms extracted components into an AST (Abstract Syntax Tree)
45
+ # @rbs method: Symbol?
46
+ # @rbs arguments_tokens: Array[Token] -- arguments tokens
47
+ # @rbs return: Node -- AST (Abstract Syntax Tree)
48
+ def transform(method:, arguments_tokens:)
49
+ method = Node.new(:name, [method], urtext: urtext, source: method)
50
+ arguments = ArgumentsParser.new(arguments_tokens).parse
51
+ source = "#{method.source}#{arguments.source}"
52
+ children = [method, arguments].reject(&:empty?)
53
+
54
+ Node.new :macro, children.reject(&:empty?), urtext: urtext, source: source
55
+ end
56
+
57
+ private
58
+
59
+ # Tokenizes source code
60
+ # @rbs code: String -- source code to tokenize
61
+ # @rbs return: Array[Token] -- wrapped ripper tokens
62
+ def tokenize(code)
63
+ tokens = Tokenizer.new(code).tokenize
64
+ macro = []
65
+
66
+ tokens.each do |token|
67
+ break if token.whitespace? && macro_finished?(macro)
68
+ macro << token
69
+ end
70
+
71
+ macro
72
+ end
73
+
74
+ # Indicates if there is a set of arguments in the tokens
75
+ # @rbs tokens: Array[Token] -- tokens to check
76
+ # @rbs return: bool
77
+ def arguments?(tokens)
78
+ arguments_started?(tokens) && arguments_finished?(tokens)
79
+ end
80
+
81
+ # Indicates if arguments have started
82
+ # @rbs tokens: Array[Token] -- tokens to check
83
+ # @rbs return: bool
84
+ def arguments_started?(tokens)
85
+ tokens.any? { _1.arguments_start? }
86
+ end
87
+
88
+ # Indicates if arguments have finished
89
+ # @note Call this after a whitespace has been detected
90
+ # @rbs tokens: Array[Token] -- tokens to check
91
+ # @rbs return: bool
92
+ def arguments_finished?(tokens)
93
+ tokens.any? { _1.arguments_finish? }
94
+ end
95
+
96
+ # Indicates if a macro token array is complete or finished
97
+ # @note Call this after a whitespace has been detected
98
+ # @rbs tokens: Array[Token] -- tokens to check
99
+ # @rbs return: bool
100
+ def finished?(tokens)
101
+ return false unless tokens.any? { _1.method_name? }
102
+ return false if arguments_started?(tokens) && !arguments_finished?(tokens)
103
+ true
104
+ end
105
+
106
+ # Indicates if a value is a Ruby native format string
107
+ # @rbs value: String -- value to check
108
+ # @rbs return: bool
109
+ def native_format_string?(value)
110
+ value.start_with? Sigils::FORMAT_PREFIX
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rbs_inline: enabled
4
+
5
+ module Fmt
6
+ # Responsible for parsing various inputs and returning an AST (Abstract Syntax Tree)
7
+ #
8
+ # Mechanics are similar to an ETL pipeline (Extract, Transform, Load), however,
9
+ # parsers only handle extracting and transforming.
10
+ #
11
+ # Loading is handled by AST processors (Models)
12
+ # @see lib/fmt/models/
13
+ class Parser
14
+ Cache = Fmt::LRUCache.new # : Fmt::LRUCache -- local in-memory cache
15
+
16
+ # Escapes a string for use in a regular expression
17
+ # @rbs value: String -- string to escape
18
+ # @rbs return: String -- escaped string
19
+ def self.esc(value) = Regexp.escape(value.to_s)
20
+
21
+ # Parses input passed to the constructor and returns an AST (Abstract Syntax Tree)
22
+ #
23
+ # 1. Extract components
24
+ # 2. Transform to AST
25
+ #
26
+ # @note Subclasses must implement the extract and transform methods
27
+ #
28
+ # @rbs return: Node -- AST (Abstract Syntax Tree)
29
+ def parse
30
+ extract.then { transform(**_1) }
31
+ end
32
+
33
+ protected
34
+
35
+ # Extracts components for building the AST (Abstract Syntax Tree)
36
+ # @rbs return: Hash[Symbol, Object] -- extracted components
37
+ def extract
38
+ raise Error, "extract must be implemented by subclass"
39
+ end
40
+
41
+ # Transforms extracted components into an AST (Abstract Syntax Tree)
42
+ # @rbs kwargs: Hash[Symbol, Object] -- extracted components
43
+ # @rbs return: Node -- AST (Abstract Syntax Tree)
44
+ def transform(**kwargs)
45
+ raise Error, "transform must be implemented by subclass"
46
+ end
47
+
48
+ # Cache helper that fetches a value from the cache
49
+ # @rbs key: String -- cache key
50
+ # @rbs block: Proc -- block to execute if the value is not found in the cache
51
+ # @rbs return: Object
52
+ def cache(key, &block)
53
+ Cache.fetch_unsafe("#{self.class.name}/#{key}") { yield }
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rbs_inline: enabled
4
+
5
+ module Fmt
6
+ # Parses a pipeline from a string and builds an AST (Abstract Syntax Tree)
7
+ class PipelineParser < Parser
8
+ # Constructor
9
+ # @rbs urtext: String -- original source code
10
+ def initialize(urtext = "")
11
+ @urtext = urtext.to_s
12
+ end
13
+
14
+ attr_reader :urtext # : String -- original source code
15
+
16
+ # Parses the urtext (original source code)
17
+ # @rbs return: Node -- AST (Abstract Syntax Tree)
18
+ def parse
19
+ cache(urtext) { super }
20
+ end
21
+
22
+ protected
23
+
24
+ # Extracts components for building the AST (Abstract Syntax Tree)
25
+ # @rbs return: Hash[Symbol, Object] -- extracted components
26
+ def extract
27
+ macros = urtext.split(Sigils::PIPE_OPERATOR).map(&:strip).reject(&:empty?)
28
+ {macros: macros}
29
+ end
30
+
31
+ # Transforms extracted components into an AST (Abstract Syntax Tree)
32
+ # @rbs macros: Array[Array[Token]] -- extracted macro tokens
33
+ # @rbs return: Node -- AST (Abstract Syntax Tree)
34
+ def transform(macros:)
35
+ macros = macros.map { |macro_urtext, memo| MacroParser.new(macro_urtext).parse }.reject(&:empty?)
36
+ source = macros.map(&:source).join(Sigils::PIPE_OPERATOR).strip
37
+
38
+ Node.new :pipeline, macros, urtext: urtext, source: source
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rbs_inline: enabled
4
+
5
+ module Fmt
6
+ # Parses a template from a string and builds an AST (Abstract Syntax Tree)
7
+ class TemplateParser < Parser
8
+ PIPELINE_HEAD = %r{(?=(?!#{esc Sigils::PIPE_OPERATOR})#{Sigils::FORMAT_PREFIX})}o # : Regexp -- detects a native Ruby format string
9
+ PIPELINE_TAIL = %r{(?=(\s+#{Sigils::FORMAT_PREFIX})|\z)}o # : Regexp -- detects a pipeline suffix
10
+
11
+ EMBED_HEAD = %r{(?=#{esc Sigils::EMBED_PREFIX})}o # : Regexp -- detects an embed prefix
12
+ EMBED_TAIL = %r{#{esc Sigils::EMBED_SUFFIX}}o # : Regexp -- detects an embed suffix
13
+
14
+ # Constructor
15
+ # @rbs urtext: String -- original source code
16
+ # @rbs scanner: StringScanner?
17
+ def initialize(urtext = "", scanner: nil)
18
+ @urtext = urtext.to_s
19
+ @scanner = scanner || StringScanner.new(@urtext)
20
+ end
21
+
22
+ attr_reader :urtext # : String -- original source code
23
+ attr_reader :scanner # : StringScanner?
24
+
25
+ # Parses the urtext (original source code)
26
+ # @rbs return: Node -- AST (Abstract Syntax Tree)
27
+ def parse
28
+ cache(urtext) { super }
29
+ end
30
+
31
+ protected
32
+
33
+ # Extracts components for building the AST (Abstract Syntax Tree)
34
+ # @note Extraction is delegated to the PipelineParser and EmbedParser in transform
35
+ # @rbs return: Hash
36
+ def extract
37
+ source = urtext
38
+
39
+ embeds = extract_embeds
40
+ embeds.each do |embed|
41
+ source = "#{source[0...embed[:index]]}#{embed[:placeholder]}#{source[embed[:rindex]..]}"
42
+ end
43
+
44
+ pipelines = extract_pipelines(source)
45
+
46
+ {embeds: embeds, pipelines: pipelines, source: source}
47
+ end
48
+
49
+ # Transforms extracted components into an AST (Abstract Syntax Tree)
50
+ # @rbs embeds: Array[Hash] -- extracted embeds
51
+ # @rbs pipelines: Array[String] -- extracted pipelines
52
+ # @rbs source: String -- parsed source code
53
+ # @rbs return: Node -- AST (Abstract Syntax Tree)
54
+ def transform(embeds:, pipelines:, source:)
55
+ embeds = embeds.map { EmbedParser.new(_1[:urtext], **_1.slice(:key, :placeholder)).parse }
56
+ embeds = Node.new(:embeds, embeds, urtext: urtext, source: urtext)
57
+
58
+ pipelines = pipelines.map { PipelineParser.new(_1).parse }
59
+ pipelines = Node.new(:pipelines, pipelines, urtext: urtext, source: source)
60
+
61
+ children = [embeds, pipelines].reject(&:empty?)
62
+
63
+ Node.new :template, children, urtext: urtext, source: source
64
+ end
65
+
66
+ private
67
+
68
+ # Extracts embed metadata from the urtext
69
+ # @rbs return: Array[Hash] -- extracted embeds
70
+ def extract_embeds
71
+ embeds = []
72
+
73
+ index = nil
74
+ embed = ""
75
+
76
+ scanner = StringScanner.new(urtext)
77
+ scanner.skip_until(EMBED_HEAD)
78
+
79
+ while scanner.matched?
80
+ index ||= scanner.charpos
81
+ embed = "#{embed}#{scanner.scan_until(EMBED_TAIL)}"
82
+
83
+ if embed.scan(EMBED_HEAD).size == embed.scan(EMBED_TAIL).size
84
+ rindex = scanner.charpos
85
+ key = :"embed_#{index}_#{rindex}"
86
+
87
+ embeds << {
88
+ index: index,
89
+ rindex: rindex,
90
+ key: key,
91
+ placeholder: "#{Sigils::FORMAT_PREFIX}#{Sigils::KEY_PREFIXES[-1]}#{key}#{Sigils::KEY_SUFFIXES[-1]}",
92
+ urtext: embed
93
+ }
94
+
95
+ index = nil
96
+ embed = scanner.skip_until(EMBED_HEAD)
97
+ end
98
+ end
99
+
100
+ embeds
101
+ end
102
+
103
+ # Extracts pipelines from the source
104
+ # @rbs source: String -- source code to extract pipelines from
105
+ # @rbs return: Array[String] -- extracted pipelines
106
+ def extract_pipelines(source)
107
+ pipelines = []
108
+ pipeline = ""
109
+
110
+ scanner = StringScanner.new(source)
111
+ scanner.skip_until(PIPELINE_HEAD)
112
+
113
+ while scanner.matched?
114
+ pipeline = scanner.scan_until(PIPELINE_TAIL)
115
+
116
+ if scanner.matched?
117
+ pipelines << pipeline
118
+ scanner.skip_until(PIPELINE_HEAD)
119
+ end
120
+ end
121
+
122
+ pipelines
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rbs_inline: enabled
4
+
5
+ module Fmt
6
+ module KernelRefinement
7
+ refine Kernel do
8
+ # Formats an object with Fmt
9
+ # @rbs object [Object] -- object to format (coerced to String)
10
+ # @rbs pipeline [Array[String | Symbol]] -- Fmt pipeline
11
+ # @rbs return [String] -- formatted text
12
+ def fmt(object, *pipeline)
13
+ text = case object
14
+ in String then object
15
+ in Symbol then object.to_s
16
+ else object.inspect
17
+ end
18
+ Fmt "%s|>to_s|>#{pipeline.join("|>")}", text
19
+ end
20
+
21
+ # Formats an object with Fmt and prints to STDOUT
22
+ # @rbs object [Object] -- object to format (coerced to String)
23
+ # @rbs pipeline [Array[String | Symbol]] -- Fmt pipeline
24
+ # @rbs return void
25
+ def fmt_print(object, *pipeline)
26
+ puts fmt(object, *pipeline)
27
+ end
28
+
29
+ # Formats an object with Fmt and puts to STDOUT
30
+ # @rbs object [Object] -- object to format (coerced to String)
31
+ # @rbs pipeline [Array[String | Symbol]] -- Fmt pipeline
32
+ # @rbs return void
33
+ def fmt_puts(object, *pipeline)
34
+ puts fmt(object, *pipeline)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rbs_inline: enabled
4
+
5
+ module Fmt
6
+ # Extends native Ruby String format specifications with native Ruby methods
7
+ # @see https://ruby-doc.org/3.3.4/format_specifications_rdoc.html
8
+ class NativeRegistry < Registry
9
+ SUPPORTED_CLASSES = [
10
+ Array,
11
+ Date,
12
+ DateTime,
13
+ FalseClass,
14
+ Float,
15
+ Hash,
16
+ Integer,
17
+ NilClass,
18
+ Range,
19
+ Regexp,
20
+ Set,
21
+ StandardError,
22
+ String,
23
+ Struct,
24
+ Symbol,
25
+ Time,
26
+ TrueClass
27
+ ].freeze
28
+
29
+ # Constructor
30
+ def initialize
31
+ super
32
+
33
+ format = ->(*args, **kwargs) do
34
+ verbose = $VERBOSE
35
+ $VERBOSE = nil
36
+ Kernel.sprintf(self, *args, **kwargs)
37
+ ensure
38
+ $VERBOSE = verbose
39
+ end
40
+
41
+ add([Kernel, :format], &format)
42
+ add([Kernel, :sprintf], &format)
43
+
44
+ SUPPORTED_CLASSES.each do |klass|
45
+ supported_method_names(klass).each do |name|
46
+ add([klass, name]) { |*args, **kwargs| public_send(name, *args, **kwargs) }
47
+ end
48
+ end
49
+ rescue => error
50
+ puts "#{self.class.name} - Error adding filters! #{error.inspect}"
51
+ end
52
+
53
+ private
54
+
55
+ # Array of supported method names for a Class
56
+ # @rbs klass: Class
57
+ # @rbs return: Array[Symbol]
58
+ def supported_method_names(klass)
59
+ klass.public_instance_methods.each_with_object(Set.new) do |name, memo|
60
+ next if name in Sigils::FORMAT_SPECIFIERS
61
+ next if name.start_with?("_") || name.end_with?("!")
62
+ memo << name
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rbs_inline: enabled
4
+
5
+ module Fmt
6
+ # Extends native Ruby String format specifications with Rainbow methods
7
+ # @see https://ruby-doc.org/3.3.4/format_specifications_rdoc.html
8
+ # @note Rainbow macros convert the Object to a String
9
+ class RainbowRegistry < Registry
10
+ # Constructor
11
+ def initialize
12
+ super
13
+
14
+ if defined? Rainbow
15
+ add([Object, :rainbow]) { Rainbow self }
16
+ add([Object, :bg]) { |*args, **kwargs| Rainbow(self).bg(*args, **kwargs) }
17
+ add([Object, :color]) { |*args, **kwargs| Rainbow(self).color(*args, **kwargs) }
18
+
19
+ methods = Rainbow::Presenter.public_instance_methods(false).select do
20
+ Rainbow::Presenter.public_instance_method(_1).arity == 0
21
+ end
22
+
23
+ method_names = methods
24
+ .map { _1.name.to_sym }
25
+ .concat(Rainbow::X11ColorNames::NAMES.keys)
26
+ .sort
27
+
28
+ method_names.each do |name|
29
+ add([Object, name]) { Rainbow(self).public_send name }
30
+ end
31
+ end
32
+ rescue => error
33
+ puts "#{self.class.name} - Error adding filters! #{error.inspect}"
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rbs_inline: enabled
4
+
5
+ module Fmt
6
+ # Registry for storing and retrieving String formatters i.e. Procs
7
+ class Registry
8
+ extend Forwardable
9
+
10
+ INSTANCE_VAR = :@fmt_registry_key # : Symbol -- instance variable set on registered Procs
11
+ private_constant :INSTANCE_VAR
12
+
13
+ # Constructor
14
+ def initialize
15
+ @store = LRUCache.new(capacity: -1)
16
+ end
17
+
18
+ def_delegator :store, :to_h # : Hash[Symbol, Proc]
19
+ def_delegator :store, :[] # : Proc -- retrieves a Proc from the registry
20
+ def_delegator :store, :key? # : bool -- indicates if a key exists in the registry
21
+
22
+ # Indicates if a method name is registered for any Class
23
+ # @rbs method_name: Symbol -- method name to check
24
+ # @rbs return: bool
25
+ def any?(method_name)
26
+ !!method_names[method_name]
27
+ end
28
+
29
+ # Indicates if a method name is unregistered
30
+ # @rbs method_name: Symbol -- method name to check
31
+ # @rbs return: bool
32
+ def none?(method_name)
33
+ !any?(method_name)
34
+ end
35
+
36
+ # Adds a keypair to the registry
37
+ # @rbs key: Array[Class | Module, Symbol] -- key to use
38
+ # @rbs overwrite: bool -- overwrite the existing keypair (default: false)
39
+ # @rbs block: Proc -- Proc to add (optional, if proc is provided)
40
+ # @rbs return: Proc
41
+ def add(key, overwrite: false, &block)
42
+ raise Error, "key must be an Array[Class | Module, Symbol]" unless key in [Class | Module, Symbol]
43
+
44
+ return store[key] if store.key?(key) && !overwrite
45
+
46
+ store.lock do
47
+ store[key] = block
48
+ block.instance_variable_set INSTANCE_VAR, key
49
+ end
50
+
51
+ block
52
+ end
53
+
54
+ # Deletes a keypair from the registry
55
+ # @rbs key: Array[Class | Module, Symbol] -- key to delete
56
+ # @rbs return: Proc?
57
+ def delete(key)
58
+ store.lock do
59
+ callable = store.delete(key)
60
+ callable&.remove_instance_variable INSTANCE_VAR
61
+ end
62
+ end
63
+
64
+ # Fetches a Proc from the registry
65
+ # @rbs key: Array[Class | Module, Symbol] -- key to retrieve
66
+ # @rbs callable: Proc -- Proc to use if the key is not found (optional, if block is provided)
67
+ # @rbs block: Proc -- block to use if the key is not found (optional, if proc is provided)
68
+ # @rbs return: Proc
69
+ def fetch(key, callable: nil, &block)
70
+ callable ||= block
71
+ store[key] || add(key, &callable)
72
+ end
73
+
74
+ # Retrieves the registered key for a Proc
75
+ # @rbs callable: Proc -- Proc to retrieve the key for
76
+ # @rbs return: Symbol?
77
+ def key_for(callable)
78
+ callable&.instance_variable_get INSTANCE_VAR
79
+ end
80
+
81
+ # Merges another registry into this one
82
+ # @rbs other: Fmt::Registry -- other registry to merge
83
+ # @rbs return: Fmt::Registry
84
+ def merge!(other)
85
+ raise Error, "other must be a registry" unless other in Registry
86
+ other.to_h.each { add(_1, &_2) }
87
+ self
88
+ end
89
+
90
+ # Executes a block with registry overrides
91
+ #
92
+ # @note Overrides will temporarily be added to the registry
93
+ # and will overwrite existing entries for the duration of the block
94
+ # Non overriden entries remain unchanged
95
+ #
96
+ # @rbs overrides: Hash[Array[Class | Module, Symbol], Proc] -- overrides to apply
97
+ # @rbs block: Proc -- block to execute with overrides
98
+ # @rbs return: void
99
+ def with_overrides(overrides, &block)
100
+ return yield unless overrides in Hash
101
+ return yield unless overrides&.any?
102
+
103
+ overrides.select! { [_1, _2] in [[Class | Module, Symbol], Proc] }
104
+ originals = store.slice(*(store.keys & overrides.keys))
105
+
106
+ store.lock do
107
+ overrides.each { add(_1, overwrite: true, &_2) }
108
+ yield
109
+ end
110
+ ensure
111
+ store.lock do
112
+ overrides&.each { delete _1 }
113
+ originals&.each { add(_1, overwrite: true, &_2) }
114
+ end
115
+ end
116
+
117
+ protected
118
+
119
+ attr_reader :store # : LRUCache
120
+
121
+ # Hash of registered method names
122
+ # @rbs return: Hash[Symbol, TrueClass]
123
+ def method_names
124
+ store.keys.each_with_object({}) { _2[_1.last] = true }
125
+ end
126
+ end
127
+ end