fmt 0.1.3 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +167 -93
  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 +50 -12
  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 -18
  56. data/lib/fmt/embed.rb +0 -19
  57. data/lib/fmt/filter.rb +0 -32
  58. data/lib/fmt/filter_groups/filter_group.rb +0 -56
  59. data/lib/fmt/filter_groups/rainbow_filter_group.rb +0 -27
  60. data/lib/fmt/filter_groups/string_filter_group.rb +0 -28
  61. data/lib/fmt/formatter.rb +0 -60
  62. data/lib/fmt/scanners/base_scanner.rb +0 -41
  63. data/lib/fmt/scanners/embed_scanner.rb +0 -56
  64. data/lib/fmt/scanners/filter_scanner.rb +0 -31
  65. data/lib/fmt/scanners/key_scanner.rb +0 -15
  66. data/lib/fmt/scanners.rb +0 -3
  67. data/lib/fmt/transformer.rb +0 -57
@@ -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