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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a4bafcbfda352b24f4203057c8ae8c8df4ddc109727fd610250d54c1db02ea2a
4
- data.tar.gz: 1e672363e323f01757e1590441d8f836bba9b03936b4551189cd040a942936f2
3
+ metadata.gz: c91008418cc9c29be346581fda313f6a498203899c932480187bb9f70bf4fb3f
4
+ data.tar.gz: 77925da927d90b09fdba3edcd595dc392f43286d4e8468792038bd1f57242cd7
5
5
  SHA512:
6
- metadata.gz: 39a4f811f2a9ee86e82fd0e7ce557aa6f22971cd31bde8c5a717b47f74b35119c8d7616580dac5de18851329edd304a72549ecd8239c2104caea3f41d4a3da7e
7
- data.tar.gz: 75fe78f4908b6b94eb7fe37ba0d76819562d7005405d72d3c14ccb93b55d465da95e890becd9794b53eb7c3d44970a6f14ba3d62d45c1eb78e1cf31810e4885f
6
+ metadata.gz: 7c440974ee5dcf7a3ef53cbbccf021f4011890f9f2d279e91aa9affb08f9032b034d9e39e3385b40a7e3988c399d0a96dff074581b8ac2ea037f081b6877fe0c
7
+ data.tar.gz: f45953ec9a6095cdf98106ca5582a24631ff2262efef979c7e5cacbd0abaf686cd59a814aa217428e87d2064060e8b5af1aa5ec97b8d40124fab65e124841a84
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  <p align="center">
2
2
  <a href="http://blog.codinghorror.com/the-best-code-is-no-code-at-all/">
3
- <img alt="Lines of Code" src="https://img.shields.io/badge/loc-316-47d299.svg" />
3
+ <img alt="Lines of Code" src="https://img.shields.io/badge/loc-1039-47d299.svg" />
4
4
  </a>
5
5
  <a href="https://github.com/testdouble/standard">
6
6
  <img alt="Ruby Style" src="https://img.shields.io/badge/style-standard-168AFE?logo=ruby&logoColor=FE1616" />
@@ -13,165 +13,248 @@
13
13
  </a>
14
14
  </p>
15
15
 
16
- # Fmt
16
+ # Fmt: Your Friendly String Formatter
17
17
 
18
- #### A simple template engine based on native Ruby String formatting mechanics
18
+ Welcome to Fmt, your go-to companion for Ruby string templating – simple, versatile, and surprisingly powerful!
19
19
 
20
20
  <!-- Tocer[start]: Auto-generated, don't remove. -->
21
21
 
22
22
  ## Table of Contents
23
23
 
24
- - [Why?](#why)
25
- - [Setup](#setup)
26
- - [Usage](#usage)
27
- - [Formatting](#formatting)
28
- - [Filters](#filters)
29
- - [Embeds](#embeds)
30
- - [Sponsors](#sponsors)
24
+ - [What's Fmt, and why should you care?](#whats-fmt-and-why-should-you-care)
25
+ - [Getting Started: It's a Breeze!](#getting-started-its-a-breeze)
26
+ - [Usage: Your Formatting Adventure Begins](#usage-your-formatting-adventure-begins)
27
+ - [Macros: The Secret Sauce](#macros-the-secret-sauce)
28
+ - [Pipelines: Unleashing the Power of Chained Macros](#pipelines-unleashing-the-power-of-chained-macros)
29
+ - [Supported Methods: The Ultimate Answer to Formatting](#supported-methods-the-ultimate-answer-to-formatting)
30
+ - [Rainbow GEM: The Color of Magic](#rainbow-gem-the-color-of-magic)
31
+ - [Composition: The Art of the Template](#composition-the-art-of-the-template)
32
+ - [Embedded Templates: Nesting with Infinite Possibilities](#embedded-templates-nesting-with-infinite-possibilities)
33
+ - [Customizing Fmt: Create Your Own Extensions](#customizing-fmt-create-your-own-extensions)
34
+ - [Performance: Fast Formatting](#performance-fast-formatting)
35
+ - [Sponsors: Our Awesome Supporters](#sponsors-our-awesome-supporters)
31
36
 
32
37
  <!-- Tocer[finish]: Auto-generated, don't remove. -->
33
38
 
34
- ## Why?
39
+ ## What's Fmt, and why should you care?
35
40
 
36
- I'm currenly using this to help build beautiful CLI applications with Ruby. Plus it's fun.
41
+ Fmt is a template engine that harnesses the raw power of Ruby's native string formatting mechanics. It's like having a universal translator for your strings, turning your formatting wishes into beautiful, functional output.
37
42
 
38
- ## Setup
43
+ - 🚀 Supercharge your general-purpose templating
44
+ - 🎨 Craft CLI applications so beautiful, they'll make even the most stoic developer smile
45
+ - 🧠 Intuitive enough for beginners, powerful enough for experts
39
46
 
40
- ```
41
- bundle add rainbow # optional
47
+ ## Getting Started: It's a Breeze!
48
+
49
+ First, let's get you set up. It's easier than making a cup of coffee!
50
+
51
+ ```sh
52
+ bundle add rainbow # <- optional, but recommended for those who enjoy a splash of color
42
53
  bundle add fmt
43
54
  ```
44
55
 
45
- ## Usage
46
-
47
- Simply create a string with embedded formatting syntax as you'd normally do with `sprintf` or `format`.
56
+ Then, in your Ruby file:
48
57
 
49
58
  ```ruby
50
- "%{...}"
59
+ require "rainbow" # <- optional, but why not add some color to your life?
60
+ require "fmt"
51
61
  ```
52
62
 
53
- Filters can be chained after the placeholder like so.
63
+ ## Usage: Your Formatting Adventure Begins
54
64
 
55
- ```ruby
56
- "%{...}FILTER|FILTER|FILTER"
57
- ```
65
+ Using Fmt is simpler than ordering takeout. Just create a string with Ruby's [format specifiers](https://ruby-doc.org/3.3.5/format_specifications_rdoc.html), much like you would for `sprintf`.
58
66
 
59
- > [!NOTE]
60
- > Filters are processed in the order they are specified.
67
+ - `"%s"` - The classic
68
+ - `"%{variable}"` - For when you're feeling descriptive
69
+ - `"%<variable>s"` - When you want to be extra explicit
61
70
 
62
- Filters can be [native Ruby formatting](https://docs.ruby-lang.org/en/master/format_specifications_rdoc.html) as well as String methods like `capitalize`, `downcase`, `strip`, etc.
63
- Also, you can use Rainbow filters like `bold`, `cyan`, `underline`, et al. if you have the [Rainbow GEM](https://github.com/ku1ik/rainbow) installed.
71
+ Remember, with Fmt, any string can be a template. It's like having a Swiss Army knife for text formatting!
64
72
 
65
- **You can even [register your own filters](#filters).**
73
+ ### Macros: The Secret Sauce
66
74
 
67
- ### Formatting
75
+ Formatting macros are what make Fmt special. Append them to your format specifiers like so:
68
76
 
69
- Basic example:
77
+ <!-- test_e798c3 -->
70
78
 
71
79
  ```ruby
72
- require "rainbow"
73
- require "fmt"
80
+ Fmt("%s|>capitalize", "hello world!") # => "Hello world!"
81
+ Fmt("%{msg}|>capitalize", msg: "hello world!") # => "Hello world!"
82
+ ```
74
83
 
75
- template = "Hello %{name}cyan|bold"
76
- Fmt template, name: "World"
84
+ Macros can accept arguments.
77
85
 
78
- #=> "Hello \e[36m\e[1mWorld\e[0m"
86
+ <!-- test_1707d2 -->
87
+
88
+ ```ruby
89
+ Fmt("%s|>prepend('Hello ')", "world!") # => "Hello world!"
90
+ Fmt("%{msg}|>prepend('Hello ')", msg: "world!") # => "Hello world!"
79
91
  ```
80
92
 
81
- ![CleanShot 2024-07-26 at 01 40 33@2x](https://github.com/user-attachments/assets/04ff90e6-254a-42d4-9169-586ac24b82f0)
93
+ ### Pipelines: Unleashing the Power of Chained Macros
94
+
95
+ Macros can be chained to create a formatting pipeline.
82
96
 
83
- Mix and match native formatting with Rainbow formatting:
97
+ <!-- test_425625 -->
84
98
 
85
99
  ```ruby
86
- require "rainbow"
87
- require "fmt"
100
+ Fmt("%s|>prepend('Hello ')|>ljust(32, '.')|>upcase", "world!") # => "HELLO WORLD!...................."
101
+ Fmt("%{msg}|>prepend('Hello ')|>ljust(32, '.')|>upcase", msg: "world!") # => "HELLO WORLD!...................."
102
+ ```
103
+
104
+ > [!NOTE]
105
+ > Pipelines are processed left to right. The return value from the preceeding macro is the starting value for the next macro.
88
106
 
89
- template = "Date: %{date}.10s|magenta"
90
- Fmt template, date: Time.now
107
+ Arguments and return values can be any type.
91
108
 
92
- #=> "Date: \e[35m2024-07-26\e[0m"
109
+ <!-- test_f55ae2 -->
110
+
111
+ ```ruby
112
+ Fmt("%p|>partition(/:/)|>last|>delete_suffix('>')", Object.new) # => "0x000000011f33bc68"
93
113
  ```
94
114
 
95
- ![CleanShot 2024-07-26 at 01 42 53@2x](https://github.com/user-attachments/assets/507913b0-826b-4526-9c79-27f766c904b3)
115
+ ### Supported Methods: The Ultimate Answer to Formatting
96
116
 
97
- Multiline example:
117
+ Most public instance methods on the following classes are supported.
98
118
 
99
- ```ruby
100
- require "rainbow"
101
- require "fmt"
119
+ - `Array`
120
+ - `Date`
121
+ - `DateTime`
122
+ - `FalseClass`
123
+ - `Float`
124
+ - `Hash`
125
+ - `Integer`
126
+ - `NilClass`
127
+ - `Range`
128
+ - `Regexp`
129
+ - `Set`
130
+ - `StandardError`
131
+ - `String`
132
+ - `Struct`
133
+ - `Symbol`
134
+ - `Time`
135
+ - `TrueClass`
102
136
 
103
- template = <<~T
104
- Date: %{date}.10s|underline
137
+ > [!TIP]
138
+ > If you're using libraries like ActiveSupport that extend these classes, extension methods will also available if the library is required before Fmt.
105
139
 
106
- Greetings, %{name}upcase|bold
140
+ #### Rainbow GEM: The Color of Magic
107
141
 
108
- %{message}strip|green
109
- T
142
+ Color and style support is available if your project includes the [Rainbow GEM](https://github.com/ku1ik/rainbow).
110
143
 
111
- Fmt template, date: Time.now, name: "Hopsoft", message: "This is neat!"
144
+ <!-- test_19c8ca -->
112
145
 
113
- #=> "Date: \e[4m2024-07-26\e[0m\n\nGreetings, \e[1mHOPSOFT\e[0m\n\n\e[32mThis is neat!\e[0m\n"
146
+ ```ruby
147
+ template = "%{msg}|>cyan|>bold|>underline"
148
+ Fmt(template, msg: "Hello World!")
149
+ #=> "\e[36m\e[1m\e[4mHello World!\e[0m"
114
150
  ```
115
151
 
116
- ![CleanShot 2024-07-26 at 01 44 30@2x](https://github.com/user-attachments/assets/8926009c-7cf1-4140-9a2a-6ed718d50926)
152
+ ### Composition: The Art of the Template
153
+
154
+ You can mix and match macros that target any type within a pipeline.
117
155
 
118
- ### Filters
156
+ Templates can also include multiple format strings with their own distinct pipelines.
119
157
 
120
- You can also add your own filters to Fmt by calling `Fmt.add_filter(:name, &block)`.
121
- The block accepts a string and should return a replacement string.
158
+ <!-- test_0dbfcd -->
122
159
 
123
160
  ```ruby
124
- require "rainbow"
125
- require "fmt"
161
+ template = "Date: %<date>.10s|>magenta -- %{msg}|>titleize|>bold"
162
+ Fmt(template, date: Time.now, msg: "this is cool")
163
+ #=> "Date: \e[35m2024-09-20\e[0m \e[1mThis Is Cool\e[0m"
164
+ ```
126
165
 
127
- Fmt.add_filter(:ljust) { |val| "".ljust 14, val.to_s }
166
+ #### Embedded Templates: Nesting with Infinite Possibilities
128
167
 
129
- template = <<~T
130
- %{head}ljust|faint
131
- %{message}bold
132
- %{tail}ljust|faint
133
- T
168
+ Embedded templates can be nested within other templates... as deep as needed!
134
169
 
135
- Fmt template, head: "#", message: "Give it a try!", tail: "#"
170
+ <!-- test_efee7a -->
136
171
 
137
- #=> "\e[2m##############\e[0m\n\e[1mGive it a try!\e[0m\n\e[2m##############\e[0m\n"
172
+ ```ruby
173
+ template = "%{msg}|>faint {{%{embed}|>bold}}"
174
+ Fmt(template, msg: "Look Ma...", embed: "I'm embedded!")
175
+ #=> "\e[2mLook Ma...\e[0m \e[1mI'm embedded!\e[0m"
138
176
  ```
139
177
 
140
- ![CleanShot 2024-07-26 at 01 46 26@2x](https://github.com/user-attachments/assets/bd1d67c6-1182-428b-be05-756f3d330f67)
178
+ Embeds can also have their own distinct pipelines.
141
179
 
142
- ### Embeds
143
-
144
- Templates can be embedded or nested within other templates... as deep as needed!
145
- Just wrap the embedded template in double curly braces: `{{EMBEDDED TEMPLATE HERE}}`
180
+ <!-- test_abb7ea -->
146
181
 
147
182
  ```ruby
148
- require "rainbow"
149
- require "fmt"
183
+ template = "%{msg}|>faint {{%{embed}|>bold}}|>underline"
184
+ Fmt(template, msg: "Look Ma...", embed: "I'm embedded!")
185
+ #=> "\e[2mLook Ma...\e[0m \e[1m\e[4mI'm embedded!\e[0m"
186
+ ```
150
187
 
151
- template = "%{value}lime {{%{embed_value}red|bold|underline}}"
152
- Fmt template, value: "Outer", embed_value: "Inner"
188
+ Embeds can be deeply nested.
153
189
 
154
- #=> "\e[38;5;46mOuter\e[0m \e[31m\e[1m\e[4mInner\e[0m"
190
+ <!-- test_79e924 -->
191
+
192
+ ```ruby
193
+ template = "%{msg}|>faint {{%{embed}|>bold {{%{deep_embed}|>red|>bold}}}}"
194
+ Fmt(template, msg: "Look Ma...", embed: "I'm embedded!", deep_embed: "And I'm deeply embedded!")
195
+ #=> "\e[2mLook Ma...\e[0m \e[1mI'm embedded!\e[0m \e[31m\e[1mAnd I'm deeply embedded!\e[0m"
155
196
  ```
156
197
 
157
- ![CleanShot 2024-07-29 at 02 42 19@2x](https://github.com/user-attachments/assets/f67dd215-b848-4a23-bd73-72822cb7d970)
198
+ Embeds can also span multiple lines.
199
+
200
+ <!-- test_054526 -->
158
201
 
159
202
  ```ruby
160
203
  template = <<~T
161
- |--%{value}yellow|bold|underline
162
- | |--{{%{inner_value}green|bold|underline
163
- | | |--{{%{deep_value}blue|bold|underline
164
- | | | |-- We're in deep!}}}}
204
+ Multiline:
205
+ %{one}|>red {{
206
+ %{two}|>blue {{
207
+ %{three}|>green
208
+ }}
209
+ }}|>bold
165
210
  T
211
+ Fmt(template, one: "Red", two: "Blue", three: "Green")
212
+ #=> "Multiline:\n\e[31mRed\e[0m \e[1m\n \e[34mBlue\e[0m \n \e[32mGreen\e[0m"
213
+ ```
214
+
215
+ ### Customizing Fmt: Create Your Own Extensions
216
+
217
+ Want to add your own filters? It's easier than you might think:
166
218
 
167
- Fmt template, value: "Outer", inner_value: "Inner", deep_value: "Deep"
219
+ <!-- test_2cacce -->
168
220
 
169
- #=> "|--\e[33m\e[1m\e[4mOuter\e[0m\n| |--\e[32m\e[1m\e[4mInner\e[0m\n| | |--\e[34m\e[1m\e[4mDeep\e[0m\n| | | |-- We're in deep!\n"
221
+ ```ruby
222
+ Fmt.register([Object, :shuffle]) { |*args, **kwargs| to_s.chars.shuffle.join }
223
+ Fmt("%s|>shuffle", "This don't make no sense.")
224
+ #=> "de.nnoTtsnh'oeek ssim a "
170
225
  ```
171
226
 
172
- ![CleanShot 2024-07-29 at 02 45 27@2x](https://github.com/user-attachments/assets/1b933bf4-a62d-4913-b817-d6c69b0e7028)
227
+ You can also run a Ruby block with temporary filters if you don't want to officially register them.
228
+
229
+ > [!TIP]
230
+ > This also allows you to override existing filters for the duration of the block.
231
+
232
+ <!-- test_7df4eb -->
233
+
234
+ ```ruby
235
+ Fmt.with_overrides([Object, :red] => proc { |*args, **kwargs| Rainbow(self).crimson.bold }) do
236
+ Fmt("%s|>red", "This is customized red!")
237
+ #=> "\e[38;5;197m\e[1mThis is customized red!\e[0m"
238
+ end
239
+
240
+ Fmt("%s|>red", "This is original red!")
241
+ #=> "\e[31mThis is original red!\e[0m"
242
+ ```
243
+
244
+ ## Performance: Fast Formatting
245
+
246
+ Fmt isn't just pretty – it's quick too!
247
+
248
+ - Tokenization: Uses StringScanner and Ripper to parse and tokenize templates
249
+ - Caching: Stores an Abstract Syntax Tree (AST) representation of each template, pipeline, and macro
250
+ - Speed: Current benchmarks show an average pipeline execution time of **under <0.3 milliseconds**
251
+
252
+ > [!NOTE]
253
+ > While Fmt is optimized for performance, remember that complex pipelines might take a tad longer.
254
+
255
+ **Happy formatting!**
173
256
 
174
- ## Sponsors
257
+ ## Sponsors: Our Awesome Supporters
175
258
 
176
259
  <p align="center">
177
260
  <em>Proudly sponsored by</em>
data/lib/fmt/boot.rb ADDED
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rbs_inline: enabled
4
+
5
+ # Standard libraries
6
+ require "date"
7
+ require "forwardable"
8
+ require "monitor"
9
+ require "ripper"
10
+ require "securerandom"
11
+ require "set"
12
+ require "singleton"
13
+ require "strscan"
14
+
15
+ # 3rd party libraries
16
+ require "ast"
17
+
18
+ # Foundational files (globals)
19
+ require_relative "sigils"
20
+ require_relative "lru_cache"
21
+ require_relative "mixins/matchable"
22
+ require_relative "node"
23
+ require_relative "renderer"
24
+ require_relative "token"
25
+ require_relative "tokenizer"
26
+ require_relative "version"
27
+
28
+ # Refinements
29
+ require_relative "refinements/kernel_refinement"
30
+
31
+ # Registries -- store of Procs that can be used with Fmt
32
+ require_relative "registries/registry" # <- base class
33
+ require_relative "registries/native_registry"
34
+ require_relative "registries/rainbow_registry"
35
+
36
+ # Parsers -- String | Object parsers that generate ASTs
37
+ require_relative "parsers/parser" # <- base class
38
+ require_relative "parsers/arguments_parser"
39
+ require_relative "parsers/macro_parser"
40
+ require_relative "parsers/embed_parser"
41
+ require_relative "parsers/pipeline_parser"
42
+ require_relative "parsers/template_parser"
43
+
44
+ # Models -- data structures build from ASTs
45
+ require_relative "models/model" # <- base class
46
+ require_relative "models/arguments"
47
+ require_relative "models/embed"
48
+ require_relative "models/macro"
49
+ require_relative "models/pipeline"
50
+ require_relative "models/template"
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rbs_inline: enabled
4
+
5
+ module Fmt
6
+ # A threadsafe fixed-size LRU in-memory cache
7
+ # Grows to capacity then evicts the least used entries
8
+ #
9
+ # @example
10
+ # cache = Fmt::Cache.new
11
+ #
12
+ # cache.put :key, "value"
13
+ # cache.get :key
14
+ # cache.delete :key
15
+ # cache.fetch :key, "default"
16
+ # cache.fetch(:key) { "default" }
17
+ #
18
+ # @example Capacity
19
+ # Fmt::Cache.capacity = 10_000
20
+ class LRUCache
21
+ include MonitorMixin
22
+
23
+ DEFAULT_CAPACITY = 5_000 # : Integer -- default capacity
24
+
25
+ # Constructor
26
+ # @rbs capacity: Integer -- max capacity (negative values are uncapped, default: 5_000)
27
+ # @rbs return: Fmt::Cache
28
+ def initialize(capacity: DEFAULT_CAPACITY)
29
+ super()
30
+ @capacity = capacity
31
+ @store = {}
32
+ end
33
+
34
+ # The cache max capacity (number of entries)
35
+ # @rbs return: Integer
36
+ def capacity
37
+ synchronize { @capacity }
38
+ end
39
+
40
+ # Set the max capacity (number of entries)
41
+ # @rbs capacity: Integer -- new max capacity
42
+ # @rbs return: Integer -- new max capacity
43
+ def capacity=(capacity)
44
+ synchronize { @capacity = capacity.to_i }
45
+ end
46
+
47
+ # Indicates if the cache is capped
48
+ # @rbs return: bool
49
+ def capped?
50
+ synchronize { capacity >= 0 }
51
+ end
52
+
53
+ # Clears the cache
54
+ # @rbs return: void
55
+ def clear
56
+ synchronize { store.clear }
57
+ end
58
+
59
+ # Deletes the entry for the specified key
60
+ # @rbs key: Object -- key to delete
61
+ # @rbs return: Object? -- the deleted value
62
+ def delete(key)
63
+ synchronize { store.delete key }
64
+ end
65
+
66
+ # Fetches the value for the specified key
67
+ # Writes the default value if the key is not found
68
+ # @rbs key: Object -- key to fetch
69
+ # @rbs default: Object -- default value to write
70
+ # @rbs block: Proc -- block to call to get the default value
71
+ # @rbs return: Object -- value
72
+ def fetch(key, default = nil, &block)
73
+ return get(key) if key?(key)
74
+ default ||= block&.call
75
+ synchronize { put key, default }
76
+ end
77
+
78
+ # Fetches a value from the cache without synchronization (not thread safe)
79
+ # @rbs key: Object -- key to fetch
80
+ # @rbs default: Object -- default value to write
81
+ # @rbs block: Proc -- block to call to get the default value
82
+ # @rbs return: Object -- value
83
+ def fetch_unsafe(key, default = nil, &block)
84
+ return store[key] if store.key?(key)
85
+ store[key] = (default || block&.call)
86
+ end
87
+
88
+ # Indicates if the cache is full
89
+ # @rbs return: bool
90
+ def full?
91
+ synchronize { capped? && store.size > capacity }
92
+ end
93
+
94
+ # Retrieves the value for the specified key
95
+ # @rbs key: Object -- key to retrieve
96
+ def get(key)
97
+ synchronize do
98
+ reposition(key) if key?(key)
99
+ store[key]
100
+ end
101
+ end
102
+
103
+ # Cache keys
104
+ # @rbs return: Array[Object]
105
+ def keys
106
+ synchronize { store.keys }
107
+ end
108
+
109
+ # Indicates if the cache contains the specified key
110
+ # @rbs key: Object -- key to check
111
+ # @rbs return: bool
112
+ def key?(key)
113
+ synchronize { store.key? key }
114
+ end
115
+
116
+ # Stores the value for the specified key
117
+ # @rbs key: Object -- key to store
118
+ # @rbs value: Object -- value to store
119
+ # @rbs return: Object -- value
120
+ def put(key, value)
121
+ synchronize do
122
+ delete key if capped? # keep keey fresh if capped
123
+ store[key] = value
124
+ store.shift if full? # resize the cache if necessary
125
+ value
126
+ end
127
+ end
128
+
129
+ # Resets the cache capacity to the default
130
+ # @rbs return: Integer -- capacity
131
+ def reset_capacity
132
+ synchronize { @capacity = DEFAULT_CAPACITY }
133
+ end
134
+
135
+ # The current size of the cache (number of entries)
136
+ # @rbs return: Integer
137
+ def size
138
+ synchronize { store.size }
139
+ end
140
+
141
+ # Returns a Hash with only the given keys
142
+ # @rbs keys: Array[Object] -- keys to include
143
+ # @rbs return: Hash[Object, Object]
144
+ def slice(*keys)
145
+ synchronize { store.slice(*keys) }
146
+ end
147
+
148
+ # Hash representation of the cache
149
+ # @rbs return: Hash[Object, Proc]
150
+ def to_h
151
+ synchronize { store.dup }
152
+ end
153
+
154
+ # Cache values
155
+ # @rbs return: Array[Object]
156
+ def values
157
+ synchronize { store.values }
158
+ end
159
+
160
+ # Executes a block with a synchronized mutex
161
+ # @rbs block: Proc -- block to execute
162
+ def lock(&block)
163
+ synchronize(&block)
164
+ end
165
+
166
+ alias_method :[], :get # : Object -- alias for get
167
+ alias_method :[]=, :put # : Object -- alias for put
168
+
169
+ private
170
+
171
+ attr_reader :store # : Hash[Object, Object]
172
+
173
+ # Moves the key to the end keeping it fresh
174
+ # @rbs key: Object -- key to reposition
175
+ # @rbs return: Object -- value
176
+ def reposition(key)
177
+ value = store.delete(key)
178
+ store[key] = value
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rbs_inline: enabled
4
+
5
+ module Fmt
6
+ module Matchable
7
+ # Hash representation of the Object (required for pattern matching)
8
+ # @rbs return: Hash[Symbol, Object]
9
+ def to_h
10
+ raise Error, "to_h must be implemented by including class"
11
+ end
12
+
13
+ # Returns a Hash representation of the object limited to the given keys
14
+ # @rbs keys: Array[Symbol] -- keys to include
15
+ # @rbs return: Hash[Symbol, Object]
16
+ def deconstruct_keys(keys = [])
17
+ to_h.select { _1 in keys }
18
+ end
19
+
20
+ # Returns an Array representation of the object
21
+ # @rbs return: Array[Object]
22
+ def deconstruct
23
+ to_h.values
24
+ end
25
+ end
26
+ end