fmt 0.1.3 → 0.3.1
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.
- checksums.yaml +4 -4
- data/README.md +179 -86
- data/lib/fmt/boot.rb +50 -0
- data/lib/fmt/lru_cache.rb +181 -0
- data/lib/fmt/mixins/matchable.rb +26 -0
- data/lib/fmt/models/arguments.rb +194 -0
- data/lib/fmt/models/embed.rb +48 -0
- data/lib/fmt/models/macro.rb +58 -0
- data/lib/fmt/models/model.rb +66 -0
- data/lib/fmt/models/pipeline.rb +47 -0
- data/lib/fmt/models/template.rb +55 -0
- data/lib/fmt/node.rb +128 -0
- data/lib/fmt/parsers/arguments_parser.rb +43 -0
- data/lib/fmt/parsers/embed_parser.rb +54 -0
- data/lib/fmt/parsers/macro_parser.rb +113 -0
- data/lib/fmt/parsers/parser.rb +56 -0
- data/lib/fmt/parsers/pipeline_parser.rb +41 -0
- data/lib/fmt/parsers/template_parser.rb +125 -0
- data/lib/fmt/refinements/kernel_refinement.rb +33 -0
- data/lib/fmt/registries/native_registry.rb +66 -0
- data/lib/fmt/registries/rainbow_registry.rb +36 -0
- data/lib/fmt/registries/registry.rb +127 -0
- data/lib/fmt/renderer.rb +132 -0
- data/lib/fmt/sigils.rb +23 -0
- data/lib/fmt/token.rb +126 -0
- data/lib/fmt/tokenizer.rb +96 -0
- data/lib/fmt/version.rb +3 -1
- data/lib/fmt.rb +50 -12
- data/sig/generated/fmt/boot.rbs +2 -0
- data/sig/generated/fmt/lru_cache.rbs +122 -0
- data/sig/generated/fmt/mixins/matchable.rbs +18 -0
- data/sig/generated/fmt/models/arguments.rbs +115 -0
- data/sig/generated/fmt/models/embed.rbs +34 -0
- data/sig/generated/fmt/models/macro.rbs +37 -0
- data/sig/generated/fmt/models/model.rbs +45 -0
- data/sig/generated/fmt/models/pipeline.rbs +31 -0
- data/sig/generated/fmt/models/template.rbs +33 -0
- data/sig/generated/fmt/node.rbs +64 -0
- data/sig/generated/fmt/parsers/arguments_parser.rbs +25 -0
- data/sig/generated/fmt/parsers/embed_parser.rbs +36 -0
- data/sig/generated/fmt/parsers/macro_parser.rbs +60 -0
- data/sig/generated/fmt/parsers/parser.rbs +44 -0
- data/sig/generated/fmt/parsers/pipeline_parser.rbs +25 -0
- data/sig/generated/fmt/parsers/template_parser.rbs +50 -0
- data/sig/generated/fmt/refinements/kernel_refinement.rbs +23 -0
- data/sig/generated/fmt/registries/native_registry.rbs +19 -0
- data/sig/generated/fmt/registries/rainbow_registry.rbs +11 -0
- data/sig/generated/fmt/registries/registry.rbs +69 -0
- data/sig/generated/fmt/renderer.rbs +70 -0
- data/sig/generated/fmt/sigils.rbs +30 -0
- data/sig/generated/fmt/token.rbs +77 -0
- data/sig/generated/fmt/tokenizer.rbs +51 -0
- data/sig/generated/fmt/version.rbs +5 -0
- data/sig/generated/fmt.rbs +41 -0
- metadata +126 -18
- data/lib/fmt/embed.rb +0 -19
- data/lib/fmt/filter.rb +0 -32
- data/lib/fmt/filter_groups/filter_group.rb +0 -56
- data/lib/fmt/filter_groups/rainbow_filter_group.rb +0 -27
- data/lib/fmt/filter_groups/string_filter_group.rb +0 -28
- data/lib/fmt/formatter.rb +0 -60
- data/lib/fmt/scanners/base_scanner.rb +0 -41
- data/lib/fmt/scanners/embed_scanner.rb +0 -56
- data/lib/fmt/scanners/filter_scanner.rb +0 -31
- data/lib/fmt/scanners/key_scanner.rb +0 -15
- data/lib/fmt/scanners.rb +0 -3
- data/lib/fmt/transformer.rb +0 -57
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 67c0fef310f41def1104325bdcf64e0e74c00be1c491bde56e8dd18f93121711
|
4
|
+
data.tar.gz: fa7af34e9c94c03ebbd28d4843a0c268fe63304cd95bfde0c0b7cbca7d550916
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b04858bb8cab7de025a11ecf941e175a698559b231c1878bbdf9f5762dcb3eb05cd822ee50b11a0739aa21ee5077ea9a448ef75a0e790e0587c39403e33ca303
|
7
|
+
data.tar.gz: eb55994dbd819d698b626ea170b56dec4177c02742f8b50da3e1d182704c7ff78043e31aece819ec47762cf6ab4297236d0698e2d80c6b5a974e19632817bccd
|
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-
|
3
|
+
<img alt="Lines of Code" src="https://img.shields.io/badge/loc-1042-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,172 +13,265 @@
|
|
13
13
|
</a>
|
14
14
|
</p>
|
15
15
|
|
16
|
-
#
|
16
|
+
# CLI Templating System and String Formatter
|
17
17
|
|
18
|
-
|
18
|
+
**Fmt** is a powerful and flexible templating system and string formatter for Ruby, designed to streamline the creation of command-line interfaces and enhance general-purpose string formatting.
|
19
19
|
|
20
20
|
<!-- Tocer[start]: Auto-generated, don't remove. -->
|
21
21
|
|
22
22
|
## Table of Contents
|
23
23
|
|
24
|
-
- [
|
25
|
-
- [Setup](#setup)
|
24
|
+
- [Getting Started](#getting-started)
|
26
25
|
- [Usage](#usage)
|
27
|
-
- [
|
28
|
-
- [
|
29
|
-
- [
|
26
|
+
- [Macros](#macros)
|
27
|
+
- [Pipelines](#pipelines)
|
28
|
+
- [Supported Methods](#supported-methods)
|
29
|
+
- [Rainbow GEM](#rainbow-gem)
|
30
|
+
- [Composition](#composition)
|
31
|
+
- [Embedded Templates](#embedded-templates)
|
32
|
+
- [Customizing Fmt](#customizing-fmt)
|
33
|
+
- [Kernel Refinement](#kernel-refinement)
|
34
|
+
- [`fmt(object, *pipeline)`](#fmtobject-pipeline)
|
35
|
+
- [`fmt_print(object, *pipeline)`](#fmt_printobject-pipeline)
|
36
|
+
- [`fmt_puts(object, *pipeline)`](#fmt_putsobject-pipeline)
|
37
|
+
- [Performance](#performance)
|
30
38
|
- [Sponsors](#sponsors)
|
31
39
|
|
32
40
|
<!-- Tocer[finish]: Auto-generated, don't remove. -->
|
33
41
|
|
34
|
-
##
|
42
|
+
## Getting Started
|
35
43
|
|
36
|
-
|
44
|
+
Install the required dependencies:
|
37
45
|
|
38
|
-
|
39
|
-
|
40
|
-
```
|
41
|
-
bundle add rainbow # <- optional
|
46
|
+
```sh
|
47
|
+
bundle add rainbow # <- optional, for color support
|
42
48
|
bundle add fmt
|
43
49
|
```
|
44
50
|
|
51
|
+
Then, require the necessary libraries in your Ruby file:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
require "rainbow" # <- optional, for color support
|
55
|
+
require "fmt"
|
56
|
+
```
|
57
|
+
|
45
58
|
## Usage
|
46
59
|
|
47
|
-
|
60
|
+
Fmt uses Ruby's native [format specifiers](https://ruby-doc.org/3.3.5/format_specifications_rdoc.html) to create templates:
|
61
|
+
|
62
|
+
- `"%s"` - Standard format specifier
|
63
|
+
- `"%{variable}"` - Named format specifier
|
64
|
+
- `"%<variable>s"` - Named format specifier _(alternative syntax)_
|
65
|
+
|
66
|
+
### Macros
|
67
|
+
|
68
|
+
Formatting macros are appended to format specifiers to modify the output:
|
69
|
+
|
70
|
+
<!-- test_e798c3 -->
|
48
71
|
|
49
72
|
```ruby
|
50
|
-
"%
|
73
|
+
Fmt("%s|>capitalize", "hello world!") # => "Hello world!"
|
74
|
+
Fmt("%{msg}|>capitalize", msg: "hello world!") # => "Hello world!"
|
51
75
|
```
|
52
76
|
|
53
|
-
|
77
|
+
Macros can accept arguments:
|
78
|
+
|
79
|
+
<!-- test_1707d2 -->
|
54
80
|
|
55
81
|
```ruby
|
56
|
-
"%
|
82
|
+
Fmt("%s|>prepend('Hello ')", "world!") # => "Hello world!"
|
83
|
+
Fmt("%{msg}|>prepend('Hello ')", msg: "world!") # => "Hello world!"
|
57
84
|
```
|
58
85
|
|
59
|
-
|
60
|
-
> Filters are processed in the order they are specified.
|
86
|
+
### Pipelines
|
61
87
|
|
62
|
-
|
63
|
-
|
88
|
+
Macros can be chained to create a formatting pipeline:
|
89
|
+
|
90
|
+
<!-- test_425625 -->
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
Fmt("%s|>prepend('Hello ')|>ljust(32, '.')|>upcase", "world!") # => "HELLO WORLD!...................."
|
94
|
+
Fmt("%{msg}|>prepend('Hello ')|>ljust(32, '.')|>upcase", msg: "world!") # => "HELLO WORLD!...................."
|
95
|
+
```
|
64
96
|
|
65
|
-
|
97
|
+
Pipelines are processed left to right, with the return value from the preceding macro serving as the starting value for the next macro.
|
66
98
|
|
67
|
-
|
99
|
+
Arguments and return values can be of any type:
|
68
100
|
|
69
|
-
|
101
|
+
<!-- test_f55ae2 -->
|
70
102
|
|
71
103
|
```ruby
|
72
|
-
|
73
|
-
|
104
|
+
Fmt("%p|>partition(/:/)|>last|>delete_suffix('>')", Object.new) # => "0x000000011f33bc68"
|
105
|
+
```
|
106
|
+
|
107
|
+
### Supported Methods
|
108
|
+
|
109
|
+
Most public instance methods on the following classes are supported:
|
74
110
|
|
75
|
-
|
111
|
+
`Array`, `Date`, `DateTime`, `FalseClass`, `Float`, `Hash`, `Integer`, `NilClass`, `Range`, `Regexp`, `Set`, `StandardError`, `String`, `Struct`, `Symbol`, `Time`, `TrueClass`
|
76
112
|
|
77
|
-
|
78
|
-
Fmt template, name: "World"
|
113
|
+
Extension methods from libraries like ActiveSupport will also be available if the library is required before Fmt.
|
79
114
|
|
80
|
-
|
115
|
+
#### Rainbow GEM
|
116
|
+
|
117
|
+
Color and style support is available if your project includes the [Rainbow GEM](https://github.com/ku1ik/rainbow):
|
118
|
+
|
119
|
+
<!-- test_19c8ca -->
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
template = "%{msg}|>cyan|>bold|>underline"
|
123
|
+
Fmt(template, msg: "Hello World!")
|
124
|
+
#=> "\e[36m\e[1m\e[4mHello World!\e[0m"
|
81
125
|
```
|
82
126
|
|
83
|
-
|
127
|
+
### Composition
|
84
128
|
|
85
|
-
|
129
|
+
Templates can include multiple format strings with distinct pipelines:
|
130
|
+
|
131
|
+
<!-- test_0dbfcd -->
|
86
132
|
|
87
133
|
```ruby
|
88
|
-
|
89
|
-
|
134
|
+
template = "Date: %<date>.10s|>magenta -- %{msg}|>titleize|>bold"
|
135
|
+
Fmt(template, date: Time.now, msg: "this is cool")
|
136
|
+
#=> "Date: \e[35m2024-09-20\e[0m \e[1mThis Is Cool\e[0m"
|
137
|
+
```
|
138
|
+
|
139
|
+
#### Embedded Templates
|
90
140
|
|
91
|
-
|
141
|
+
Embedded templates can be nested within other templates:
|
92
142
|
|
93
|
-
|
94
|
-
Fmt template, date: Time.now
|
143
|
+
<!-- test_efee7a -->
|
95
144
|
|
96
|
-
|
145
|
+
```ruby
|
146
|
+
template = "%{msg}|>faint {{%{embed}|>bold}}"
|
147
|
+
Fmt(template, msg: "Look Ma...", embed: "I'm embedded!")
|
148
|
+
#=> "\e[2mLook Ma...\e[0m \e[1mI'm embedded!\e[0m"
|
97
149
|
```
|
98
150
|
|
99
|
-
|
151
|
+
Embeds can have their own pipelines:
|
100
152
|
|
101
|
-
|
153
|
+
<!-- test_abb7ea -->
|
102
154
|
|
103
155
|
```ruby
|
104
|
-
|
105
|
-
|
156
|
+
template = "%{msg}|>faint {{%{embed}|>bold}}|>underline"
|
157
|
+
Fmt(template, msg: "Look Ma...", embed: "I'm embedded!")
|
158
|
+
#=> "\e[2mLook Ma...\e[0m \e[1m\e[4mI'm embedded!\e[0m"
|
159
|
+
```
|
106
160
|
|
107
|
-
|
161
|
+
Embeds can be deeply nested:
|
108
162
|
|
109
|
-
|
110
|
-
Date: %{date}.10s|underline
|
163
|
+
<!-- test_79e924 -->
|
111
164
|
|
112
|
-
|
165
|
+
```ruby
|
166
|
+
template = "%{msg}|>faint {{%{embed}|>bold {{%{deep_embed}|>red|>bold}}}}"
|
167
|
+
Fmt(template, msg: "Look Ma...", embed: "I'm embedded!", deep_embed: "And I'm deeply embedded!")
|
168
|
+
#=> "\e[2mLook Ma...\e[0m \e[1mI'm embedded!\e[0m \e[31m\e[1mAnd I'm deeply embedded!\e[0m"
|
169
|
+
```
|
113
170
|
|
114
|
-
|
115
|
-
T
|
171
|
+
Embeds can also span multiple lines:
|
116
172
|
|
117
|
-
|
173
|
+
<!-- test_054526 -->
|
118
174
|
|
119
|
-
|
175
|
+
```ruby
|
176
|
+
template = <<~T
|
177
|
+
Multiline:
|
178
|
+
%{one}|>red {{
|
179
|
+
%{two}|>blue {{
|
180
|
+
%{three}|>green
|
181
|
+
}}
|
182
|
+
}}|>bold
|
183
|
+
T
|
184
|
+
Fmt(template, one: "Red", two: "Blue", three: "Green")
|
185
|
+
#=> "Multiline:\n\e[31mRed\e[0m \e[1m\n \e[34mBlue\e[0m \n \e[32mGreen\e[0m"
|
120
186
|
```
|
121
187
|
|
122
|
-
|
188
|
+
### Customizing Fmt
|
123
189
|
|
124
|
-
|
190
|
+
Add custom filters by registering them with Fmt:
|
125
191
|
|
126
|
-
|
127
|
-
The block accepts a string and should return a replacement string.
|
192
|
+
<!-- test_2cacce -->
|
128
193
|
|
129
194
|
```ruby
|
130
|
-
|
131
|
-
|
195
|
+
Fmt.register([Object, :shuffle]) { |*args, **kwargs| to_s.chars.shuffle.join }
|
196
|
+
Fmt("%s|>shuffle", "This don't make no sense.")
|
197
|
+
#=> "de.nnoTtsnh'oeek ssim a "
|
198
|
+
```
|
132
199
|
|
133
|
-
|
134
|
-
Fmt.add_filter(:ljust) { |val| "".ljust 14, val.to_s }
|
200
|
+
Run a Ruby block with temporary filters without officially registering them:
|
135
201
|
|
136
|
-
|
137
|
-
%{head}ljust|faint
|
138
|
-
%{message}bold
|
139
|
-
%{tail}ljust|faint
|
140
|
-
T
|
202
|
+
<!-- test_7df4eb -->
|
141
203
|
|
142
|
-
|
204
|
+
```ruby
|
205
|
+
Fmt.with_overrides([Object, :red] => proc { |*args, **kwargs| Rainbow(self).crimson.bold }) do
|
206
|
+
Fmt("%s|>red", "This is customized red!")
|
207
|
+
#=> "\e[38;5;197m\e[1mThis is customized red!\e[0m"
|
208
|
+
end
|
143
209
|
|
144
|
-
|
210
|
+
Fmt("%s|>red", "This is original red!")
|
211
|
+
#=> "\e[31mThis is original red!\e[0m"
|
145
212
|
```
|
146
213
|
|
147
|
-
|
214
|
+
## Kernel Refinement
|
215
|
+
|
216
|
+
Fmt provides a kernel refinement that adds convenient methods for formatting and outputting text directly. To use these methods, you need to enable the refinement in your code:
|
217
|
+
|
218
|
+
```ruby
|
219
|
+
using Fmt::KernelRefinement
|
220
|
+
```
|
148
221
|
|
149
|
-
|
222
|
+
Once enabled, you'll have access to the following methods:
|
150
223
|
|
151
|
-
|
152
|
-
|
224
|
+
### `fmt(object, *pipeline)`
|
225
|
+
|
226
|
+
This method formats an object using a different pipeline syntax:
|
153
227
|
|
154
228
|
```ruby
|
155
|
-
|
156
|
-
|
229
|
+
fmt("Hello, World!", :bold) # => "\e[1mHello, World!\e[0m"
|
230
|
+
fmt(:hello, :underline) # => "\e[4mhello\e[0m"
|
231
|
+
fmt(Object.new, :red) # => "\e[31m#<Object:0x00007f9b8b0b0a08>\e[0m"
|
232
|
+
```
|
157
233
|
|
158
|
-
|
234
|
+
### `fmt_print(object, *pipeline)`
|
159
235
|
|
160
|
-
|
161
|
-
Fmt template, value: "Outer", embed_value: "Inner"
|
236
|
+
This method formats an object and prints it to STDOUT without a newline:
|
162
237
|
|
163
|
-
|
238
|
+
```ruby
|
239
|
+
fmt_print("Hello, World!", :italic) # Prints: "\e[3mHello, World!\e[0m"
|
240
|
+
fmt_print(:hello, :green) # Prints: "\e[32mhello\e[0m"
|
164
241
|
```
|
165
242
|
|
166
|
-
|
243
|
+
### `fmt_puts(object, *pipeline)`
|
244
|
+
|
245
|
+
This method formats an object and prints it to STDOUT with a newline:
|
167
246
|
|
168
247
|
```ruby
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
| | |--{{%{deep_value}blue|bold|underline
|
173
|
-
| | | |-- We're in deep!}}}}
|
174
|
-
T
|
248
|
+
fmt_puts("Hello, World!", :bold, :underline) # Prints: "\e[1m\e[4mHello, World!\e[0m\n"
|
249
|
+
fmt_puts(:hello, :magenta) # Prints: "\e[35mhello\e[0m\n"
|
250
|
+
```
|
175
251
|
|
176
|
-
Fmt
|
252
|
+
These methods provide a convenient way to use Fmt's formatting capabilities directly in your code without explicitly calling the `Fmt` method.
|
177
253
|
|
178
|
-
|
254
|
+
You can pass any number of macros when using these methods:
|
255
|
+
|
256
|
+
```ruby
|
257
|
+
fmt("Important!", :red, :bold, :underline)
|
258
|
+
# => "\e[31m\e[1m\e[4mImportant!\e[0m"
|
259
|
+
|
260
|
+
fmt_puts("Warning:", :yellow, :italic)
|
261
|
+
# Prints: "\e[33m\e[3mWarning:\e[0m\n"
|
179
262
|
```
|
180
263
|
|
181
|
-
|
264
|
+
These kernel methods make it easy to integrate Fmt's powerful formatting capabilities into your command-line interfaces or any part of your Ruby application where you need to format and output text.
|
265
|
+
|
266
|
+
## Performance
|
267
|
+
|
268
|
+
Fmt is optimized for performance:
|
269
|
+
|
270
|
+
- Tokenization: Uses StringScanner and Ripper to parse and tokenize templates
|
271
|
+
- Caching: Stores an Abstract Syntax Tree (AST) representation of each template, pipeline, and macro
|
272
|
+
- Speed: Current benchmarks show an average pipeline execution time of under 0.3 milliseconds
|
273
|
+
|
274
|
+
Complex pipelines may take slightly longer to execute.
|
182
275
|
|
183
276
|
## Sponsors
|
184
277
|
|
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
|