superfluid 0.0.0 → 0.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 46ef69a407c3fbeaa0e095f67b3cf42bf8862e4fc6d9a8327807619d6058bdcc
4
- data.tar.gz: fddb30cd353a521163f394fd9fc3b5b30235d19a3e48be6357d791d89a7ec13e
3
+ metadata.gz: 5048f50c8bdaff6ed69041a2c0fac3deaf00c77df45031249f4afdb2b1543279
4
+ data.tar.gz: bd9a01bc4d9005f580e50970d9f3ba008ee0e3dc10ece706b8cc52a7a3120cef
5
5
  SHA512:
6
- metadata.gz: 900585c327e6a3539a6feb06be51b7d217b2b6bb8712ac7d436ff9d37ff774b12f4c6be89b9f4b7170b8f7b0779664fa43ea3e64b940419ec7a4620f19eafb55
7
- data.tar.gz: 0e398a2b3683039d6b247ea8b1fa33d75491b702d938534918d969ea86b1d8f1ce1daac4295d8de0ef5283228d7e3e3ff3771bac0f636a79d58d8fc39b2a23d1
6
+ metadata.gz: 44aed258d521395ad2ec7352d26eda342f1262a56efdc3431f51a9f5ca59ed02601ae7d9a31cf72c2394d62c8cac7092a2d7e04228af63ed43d818db39928798
7
+ data.tar.gz: 4a08908b8979771fe0dfe4840990764f1bd1f4bd9b0a1ff744f50f6e887586da19202241bf7b1bd722ef3959fdff11f6f7388c330f3bd7ae959bbf2f6654eddb
checksums.yaml.gz.sig CHANGED
Binary file
data/README.adoc CHANGED
@@ -3,19 +3,25 @@
3
3
  :figure-caption!:
4
4
 
5
5
  :command_pattern_link: link:https://alchemists.io/articles/command_pattern[Command Pattern]
6
+ :containable_link: link:https://alchemists.io/projects/containable[Containable]
6
7
  :core_link: link:https://alchemists.io/projects/core[Core]
8
+ :functionable_link: link:https://alchemists.io/projects/functionable[Functionable]
7
9
  :liquid_link: link:https://shopify.github.io/liquid/[Liquid]
8
- :structs_link: link:https://alchemists.io/articles/ruby_structs[Structs]
10
+ :struct_link: link:https://alchemists.io/articles/ruby_structs[Struct]
9
11
 
10
12
  = Superfluid
11
13
 
14
+ Enhances {liquid_link} by smoothing out the rough edges with a strong focus on object composition and functional design. This includes a fully functional filter and tag registry so you can reuse lower level filters and tags within all aspects of your view layer.
15
+
12
16
  toc::[]
13
17
 
14
18
  == Features
15
19
 
16
- * Simple renderer initialization.
17
- * Powerful, and customizable, default environment with a cleaner Object API.
18
- * Ability to register filters as commands (see {command_pattern_link} for details).
20
+ * Uses a customizable default environment with a cleaner Object API.
21
+ * Uses {containable_link} for filter and tag registries.
22
+ * Registers filters via a `Module`, {functionable_link}, commands (see {command_pattern_link} for details), or containers (see {containable_link} for details).
23
+ * Registers tags via classes or containers (see {containable_link} for details).
24
+ * Provides a default renderer for quick rendering of templates and associated data.
19
25
 
20
26
  == Requirements
21
27
 
@@ -63,18 +69,47 @@ The quickest way to get started is to create a new instance:
63
69
  renderer = Superfluid.new
64
70
  ----
65
71
 
66
- Now you can render your template and associated data:
72
+ Then you can render your template and associated data:
67
73
 
68
74
  [source,ruby]
69
75
  ----
70
- renderer.call "Wow, that was {{ value }}!", "value" => "quick"
76
+ renderer.call "Hi, this is a {{ value }}.", "value" => "demo"
71
77
  ----
72
78
 
73
79
  The above will yield the following output:
74
80
 
75
81
  ----
76
- "Wow, that was quick!"
82
+ "Hi, this is a demo."
83
+ ----
84
+
85
+ The following sections detail how you can configure your environment, register filters, and register tags.
86
+
87
+ === Configuration
88
+
89
+ The configuration is used by the environment. This is created for you automatically but if you want an instance of the default configuration, use:
90
+
91
+ [source,ruby]
77
92
  ----
93
+ configuration = Superfluid::Configuration.new
94
+
95
+ #<Struct:Superfluid::Configuration:0x00001780
96
+ default_resource_limits = {},
97
+ error_mode = :strict,
98
+ exception_renderer = #<Proc:0x0000000131200ee0 (lambda)>,
99
+ file_system = #<data Superfluid::Systems::Memory:0x000017b0 templates = {}>,
100
+ filter_registry = #<Superfluid::Registries::Filter:0x000000012f5a1290 @container=Superfluid::Filters::Container, @mode=:strict>,
101
+ tag_registry = #<Superfluid::Registries::Tag:0x000000012f5860d0 @container=Superfluid::Tags::Container>
102
+ >
103
+ ----
104
+
105
+ As you can see, the configuration is a {struct_link} which means you can customize using any of these attributes:
106
+
107
+ * `default_resource_limits`: Limits the resources that a template can consume. Default: `{}`.
108
+ * `error_mode`: The error mode (link:https://github.com/Shopify/liquid#error-modes[details]). Default: `:strict`.
109
+ * `exception_renderer`: The function used to render error messages. Default: `Core::Identity` (see {core_link}).
110
+ * `file_system`: The file system. Default: `Superfluid::Systems::Memory.new`.
111
+ * `filter_registry`: The filter registry. Default: `Superfluid::Registries::Filter.new`.
112
+ * `tag_registry`: The tag registry. Default: `Superfluid::Registries::Tag.new`.
78
113
 
79
114
  === Environments
80
115
 
@@ -85,100 +120,455 @@ To build the default environment, use:
85
120
  environment = Superfluid.build
86
121
  ----
87
122
 
88
- You can also build the default environment directly (which is what the above does:
123
+ To build the default environment directly (which is what the above does), use:
89
124
 
90
125
  [source,ruby]
91
126
  ----
92
- environment = Superfluid::Environment.for
127
+ environment = Superfluid::Environment.new
93
128
  ----
94
129
 
95
- Both of the above produce a _frozen_ environment (including frozen tags). If you need an environment that isn't frozen (tags included), use:
130
+ If you'd like to build a custom environment, you can do this several ways. For demonstration purposes, only the `error_mode` is used below but all configuration attributes are accepted:
96
131
 
97
132
  [source,ruby]
98
133
  ----
99
- environment = Superfluid::Environment.new
134
+ environment = Superfluid.build error_mode: :lax
135
+ environment = Superfluid::Environment.for error_mode: :lax
136
+ environment = Superfluid::Environment.new Superfluid::Configuration[error_mode: :lax]
100
137
  ----
101
138
 
102
- You can, later, freeze the environment once you've registered any/all filters and tags as follows:
139
+ You can also freeze the environment -- which also freezes the filter and tag registries -- once you've registered any/all filters and tags as follows:
103
140
 
104
141
  [source,ruby]
105
142
  ----
106
143
  environment.freeze
107
144
  ----
108
145
 
109
- Finally, the environment is an instance of a `Struct` which means you can leverage all of the power of {structs_link}.
146
+ === Filters
110
147
 
111
- When using `Superfluid.build`, `Superfluid::Environment.for`, or `Superfluid::Environment.new`, you can customize behavior via the following keyword arguments:
148
+ By default, {liquid_link} only allows registration of filters as instance methods on a namespace (`Module`). Unfortunately, this leads to terrible design, bad practices, and unnecessarily hard to test objects. You can still register namespaces but this gem encourages the use of commands (see {command_pattern_link} for further details) and containers via {containable_link} which allows you tap into Functional Programming by using procs, lambdas, and any object that responds to `#call`. This allows you to build -- and reuse -- more powerful filters. The following details the different kinds of filters you can use, each broken down by category.
112
149
 
113
- * `default_resource_limits`: The resource limits you want to set. Default: `{}`.
114
- * `error_mode`: The error mode. Default: `:strict`.
115
- * `exception_renderer`: The exception renderer. Default: `Core::Identity` (see {core_link} for details).
116
- * `file_system`: The file system. Default: `Superfluid::Systems::Memory.new`.
117
- * `filter_registry`: The filter registry. Default: `Superfluid::Registries::Filter.new`.
118
- * `tags`: The default tags. Default: `Liquid::Tags::STANDARD_TAGS`.
150
+ ==== Defaults
119
151
 
120
- === Filters
152
+ Default filters are provided for you automatically and can be viewed via your environment. Example:
153
+
154
+ [source,ruby]
155
+ ----
156
+ environment = Superfluid.build
157
+ environment.filter_registry.names
158
+ ----
159
+
160
+ The above will produce a list of all registered filters. These including all filters provided by {liquid_link} and those unique to this gem which are:
161
+
162
+ *jsonify*
163
+
164
+ Renders objects as JSON. Example:
165
+
166
+ [source,ruby]
167
+ ----
168
+ renderer.call(%({{ data | jsonify }}), {"data" => {"a" => 1}})
169
+ # "{\"a\":1}"
170
+ ----
171
+
172
+ *parse_json*
173
+
174
+ Parses JSON as a primitive. Example:
175
+
176
+ [source,ruby]
177
+ ----
178
+ renderer.call(
179
+ "{% assign data = payload | parse_json %}{{ data.one }}",
180
+ {"payload" => %({"one": 1, "two": 2})}
181
+ )
182
+
183
+ # "1"
184
+ ----
185
+
186
+ *pluralize*
187
+
188
+ Renders a plural string. Example:
189
+
190
+ [source,ruby]
191
+ ----
192
+ renderer.call(%({{ text | pluralize: 0 }}), {"text" => "apple"})
193
+ # "0 apples"
194
+
195
+ renderer.call(%({{ text | pluralize: 1 }}), {"text" => "apple"})
196
+ # "1 apple"
197
+
198
+ renderer.call(%({{ text | pluralize: 2 }}), {"text" => "apple"})
199
+ # "2 apples"
200
+
201
+ renderer.call(%({{ text | pluralize: 3, "i", "us" }}), {"text" => "octopus"})
202
+ # "3 octopi"
203
+ ----
204
+
205
+ *singularize*
206
+
207
+ Renders a singular string. Example:
208
+
209
+ [source,ruby]
210
+ ----
211
+ renderer.call(%({{ text | singularize: 0 }}), {"text" => "apples"})
212
+ # "0 apples"
213
+
214
+ renderer.call(%({{ text | singularize: 1 }}), {"text" => "apples"})
215
+ # "1 apple"
216
+
217
+ renderer.call(%({{ text | singularize: 2 }}), {"text" => "apples"})
218
+ # "2 apples"
219
+
220
+ renderer.call(%({{ text | singularize: 1, "i", "us" }}), {"text" => "octopi"})
221
+ # "1 octopus"
222
+ ----
223
+
224
+ *trim_end*
225
+
226
+ Renders string with trimmed end. Example:
227
+
228
+ [source,ruby]
229
+ ----
230
+ renderer.call(%({{ text | trim_end: 10 }}), {"text" => "A demo."})
231
+ # "A demo."
232
+
233
+ renderer.call(%({{ text | trim_end: 10 }}), {"text" => "This is a demo."})
234
+ # "This is..."
235
+
236
+ renderer.call(%({{ text | trim_end: 10, "", "---" }}), {"text" => "This is a demo."})
237
+ # "This is---"
238
+
239
+ renderer.call(%({{ text | trim_end: 10, "", "" }}), {"text" => "This is a demo."})
240
+ # "This is a"
241
+ ----
242
+
243
+ *trim_middle*
244
+
245
+ Renders string with trimmed middle. Example:
246
+
247
+ [source,ruby]
248
+ ----
249
+ renderer.call(%({{ text | trim_middle: 20 }}), {"text" => "This is a demo."})
250
+ # "A demo."
251
+
252
+ renderer.call(%({{ text | trim_middle: 13 }}), {"text" => "This is a demo."})
253
+ # "This...demo."
254
+
255
+ renderer.call(%({{ text | trim_middle: 13, "--" }}), {"text" => "This is a demo."})
256
+ # "This--demo."
257
+ ----
258
+
259
+ ==== Modules
260
+
261
+ Module instance methods are what {liquid_link} supports by default. You only need to define instance methods within a module and then register your module. Example:
262
+
263
+ [source,ruby]
264
+ ----
265
+ # Namespaces
266
+ module Primary
267
+ def echo(text) = text
268
+ end
269
+
270
+ module Secondary
271
+ def capitalize(text) = text.capitalize
272
+
273
+ def suffix(text, count = 1) = "#{text}-#{count}"
274
+ end
275
+
276
+ # Renderer
277
+ renderer = Superfluid.new { it.register_filters Primary, Secondary }
278
+
279
+ # Results
280
+ renderer.call %({{ "demo" | echo }}) # "demo"
281
+ renderer.call %({{ "demo" | capitalize }}) # "Demo"
282
+ renderer.call %({{ "demo" | suffix }}) # "demo-1"
283
+ renderer.call %({{ "demo" | suffix: 2 }}) # "demo-2"
284
+ ----
285
+
286
+ As you can see, all three methods via the two namespaces are properly registered and immediately available for use. You'll also notice multiple namespaces were registered at once but you can register them individually and/or chain them. Examples:
287
+
288
+ [source,ruby]
289
+ ----
290
+ Superfluid.new do |environment|
291
+ # Single.
292
+ environment.register_filter Primary
293
+
294
+ # Multiple.
295
+ environment.register_filters Primary, Secondary
296
+
297
+ # Chained.
298
+ environment.register_filter(Primary)
299
+ .register_filter(Secondary)
300
+ .register_filters(Primary, Secondary)
301
+ end
302
+ ----
303
+
304
+ ==== Functionables
305
+
306
+ {functionable_link} modules are identical to the module examples, as shown above, except you require and extend instead. Example:
307
+
308
+ [source,ruby]
309
+ ----
310
+ require "functionable"
311
+
312
+ # Namespaces
313
+ module Primary
314
+ extend Functionable
315
+
316
+ def echo(text) = text
317
+ end
318
+
319
+ module Secondary
320
+ extend Functionable
321
+
322
+ def capitalize(text) = text.capitalize
121
323
 
122
- By default, {liquid_link} only allows registration of filters as instance methods on a namespace (`Module`). Unfortunately, this leads to terrible design, bad practices, and unnecessarily hard to test objects. You can still register namespaces but this gem encourages the use of commands (see {command_pattern_link}) which allows you tap into Functional Programming by using procs, lambdas, and any object that responds to `#call`. This allow you to build -- and reuse -- more powerful filters.
324
+ def suffix(text, count = 1) = "#{text}-#{count}"
325
+ end
326
+ ----
327
+
328
+ This allows you to use purely functionable methods (see {functionable_link} documentation for details). At this point, you can register your functionable namespaces as follows:
329
+
330
+ [source,ruby]
331
+ ----
332
+ # Renderer
333
+ renderer = Superfluid.new { it.register_filters Primary, Secondary }
334
+
335
+ # Results
336
+ renderer.call %({{ "demo" | echo }}) # "demo"
337
+ renderer.call %({{ "demo" | capitalize }}) # "Demo"
338
+ renderer.call %({{ "demo" | suffix }}) # "demo-1"
339
+ renderer.call %({{ "demo" | suffix: 2 }}) # "demo-2"
340
+ ----
341
+
342
+ Registration is identical to modules:
343
+
344
+ [source,ruby]
345
+ ----
346
+ Superfluid.new do |environment|
347
+ # Single.
348
+ environment.register_filter Primary
349
+
350
+ # Multiple.
351
+ environment.register_filters Primary, Secondary
352
+
353
+ # Chained.
354
+ environment.register_filter(Primary)
355
+ .register_filter(Secondary)
356
+ .register_filters(Primary, Secondary)
357
+ end
358
+ ----
359
+
360
+ ==== Procs and Lambdas
361
+
362
+ Procs and lambdas are not supported by {liquid_link} but are via this gem. Example:
363
+
364
+ [source,ruby]
365
+ ----
366
+ # Setup
367
+
368
+ echo = proc { it }
369
+ capitalize = -> text { text.capitalize }
370
+ suffix = -> text, count = 1 { "#{text}-#{count}" }
371
+
372
+ # Renderer
373
+ renderer = Superfluid.new { it.register_filters echo:, capitalize:, suffix: }
374
+
375
+ # Results
376
+
377
+ renderer.call %({{ "demo" | echo }}) # "demo"
378
+ renderer.call %({{ "demo" | capitalize }}) # "Demo"
379
+ renderer.call %({{ "demo" | suffix }}) # "demo-1"
380
+ renderer.call %({{ "demo" | suffix: 2 }}) # "demo-2"
381
+ ----
382
+
383
+ Registration can be singular, plural, or chained:
123
384
 
124
385
  [source,ruby]
125
386
  ----
126
- primary = Module.new { def one = 1 }
387
+ renderer = Superfluid.new do |environment|
388
+ # Single.
389
+ environment.register_filter(echo:)
127
390
 
128
- secondary = Module.new do
129
- def two = 2
391
+ # Multiple.
392
+ environment.register_filters(echo:, capitalize:, suffix:)
130
393
 
131
- def three = 3
394
+ # Chained.
395
+ environment.register_filter(echo:)
396
+ .register_filter(capitalize:)
397
+ .register_filters(echo:, capitalize:, suffix:)
132
398
  end
399
+ ----
400
+
401
+ ==== Classes
402
+
403
+ Classes, especially composable classes, are not supported by {liquid_link} but are via this gem. You only need to ensure you include `Core::Composable` and implement the `#call` method. Example:
404
+
405
+ [source,ruby]
406
+ ----
407
+ require "core"
133
408
 
134
- function_a = proc { "four" }
135
- function_b = proc { "five" }
409
+ class Suffixer
410
+ include Core::Composable
136
411
 
137
- class Sayer
138
- def initialize prefix = "Demo"
139
- @prefix = prefix
412
+ def initialize default: 1
413
+ @default = default
140
414
  end
141
415
 
142
- def call(message) = "#{prefix}: #{message}"
416
+ def call(text, suffix = default) = "#{text}-#{suffix}"
143
417
 
144
418
  private
145
419
 
146
- attr_reader :prefix
420
+ attr_reader :default
147
421
  end
422
+ ----
423
+
424
+ Now you can register and render with the above class as follows:
425
+
426
+ [source,ruby]
427
+ ----
428
+ # Renderer
429
+ renderer = Superfluid.new { it.register_filters suffix: Suffixer.new }
430
+
431
+ # Results
148
432
 
149
- environment = Superfluid.build do |instance|
150
- instance.register_filter(primary)
151
- .register_filter(primary, four: function_a, sayer: Sayer.new)
152
- .register_filters(primary, secondary, four: function_a, five: function_b)
433
+ renderer.call %({{ "demo" | suffix }}) # "demo-1"
434
+ renderer.call %({{ "demo" | suffix: 2 }}) # "demo-2"
435
+ ----
436
+
437
+ Registration can be singular, plural, or chained:
438
+
439
+ [source,ruby]
440
+ ----
441
+ suffix = Suffixer.new
442
+
443
+ renderer = Superfluid.new do |environment|
444
+ # Single.
445
+ environment.register_filter(suffix:)
446
+
447
+ # Multiple.
448
+ environment.register_filters(suffix:)
449
+
450
+ # Chained.
451
+ environment.register_filter(suffix:)
452
+ .register_filters(suffix:)
153
453
  end
154
454
  ----
155
455
 
156
- While the above is contrived, duplicate keys are ignored. Use of `#register_filter` is for backwards compatibility with {liquid_link}. All you need is `#register_filters` to register both namespaces (modules) and commands.
456
+ ==== Containers
457
+
458
+ Containers are not supported by {liquid_link} but are via this gem using {containable_link}. To use, create a container and then register your dependencies:
459
+
460
+ [source,ruby]
461
+ ----
462
+ require "containable"
463
+
464
+ module Container
465
+ extend Containable
466
+
467
+ register(:echo) { |text| text }
468
+ register(:suffix) { |text, count = 1| "#{text}-#{count}" }
469
+ end
470
+ ----
471
+
472
+ Now you can merge the container as follows:
473
+
474
+ [source,ruby]
475
+ ----
476
+ renderer = Superfluid.new { it.merge_filters Container }
477
+ ----
478
+
479
+ You can also selectively merge by supplying only the filters you care about from the container:
480
+
481
+ [source,ruby]
482
+ ----
483
+ renderer = Superfluid.new { it.merge_filters Container, :suffix }
484
+ ----
485
+
486
+ Once registered, then you can use as follows:
487
+
488
+ [source,ruby]
489
+ ----
490
+ renderer.call %({{ "demo" | echo }}) # "demo"
491
+ renderer.call %({{ "demo" | suffix }}) # "demo-1"
492
+ renderer.call %({{ "demo" | suffix: 2 }}) # "demo-2"
493
+ ----
494
+
495
+ With containers, you have the full capabilities of the {containable_link}. The above only scratches the surface of what's possible.
157
496
 
158
497
  === Tags
159
498
 
160
- As per {liquid_link} documentation, tags only need to inherit from `Liquid::Tag` and be initialized with `tag_name`, `factor`, and `tokens`. Then you can implement a render method which accepts a `context`. Example:
499
+ As per {liquid_link} documentation, tags only need to inherit from `Liquid::Tag` and be initialized with `tag_name`, `factor`, and `tokens` parameters. Then you can implement a render method which accepts a `context`. Example:
161
500
 
162
501
  [source,ruby]
163
502
  ----
164
- class Multiply < Liquid::Tag
165
- def initialize(tag_name, factor, tokens)
166
- super
167
- @factor = factor.to_f
168
- end
503
+ # Tag
504
+ class Sample < Liquid::Block
505
+ def render(context) = super.sub("<placeholder>", rand(100).to_s)
506
+ end
169
507
 
170
- def render(context) = (factor * context["multiply_by"].to_f).to_s
508
+ # Renderer
509
+ renderer = Superfluid.new { it.register_tag :sample, Sample }
171
510
 
172
- private
511
+ # Results
512
+ renderer.call "{% sample %}Your value is: <placeholder>.{% endsample %}"
513
+ # "Your value is: 80."
514
+ ----
173
515
 
174
- attr_reader :factor
516
+ You can also register single or multiple tags at once:
517
+
518
+ [source,ruby]
519
+ ----
520
+ renderer = Superfluid.new do |environment|
521
+ # Single.
522
+ environment.register_tag(:sample, Sample)
523
+
524
+ # Multiple.
525
+ environment.register_tags(one: Sample, two: Sample)
526
+
527
+ # Chained.
528
+ environment.register_tag(:sample, Sample)
529
+ .register_tags(one: Sample, two: Sample)
175
530
  end
531
+ ----
532
+
533
+ ==== Containers
176
534
 
177
- environment = Superfluid.build do |instance|
178
- instance.register_tag(:multiply, Multiply)
535
+ Containers are not supported by {liquid_link} but are via this gem using {containable_link}. To use, create a container and then register your dependencies:
536
+
537
+ [source,ruby]
538
+ ----
539
+ require "containable"
540
+
541
+ module Container
542
+ extend Containable
543
+
544
+ register :sample, Sample
179
545
  end
180
546
  ----
181
547
 
548
+ Now you can merge the container as follows:
549
+
550
+ [source,ruby]
551
+ ----
552
+ renderer = Superfluid.new { it.merge_tags Container }
553
+ ----
554
+
555
+ You can also selectively merge by supplying only the tags you care about from the container:
556
+
557
+ [source,ruby]
558
+ ----
559
+ renderer = Superfluid.new { it.merge_tags Container, :sample }
560
+ ----
561
+
562
+ Once registered, then you can use as follows:
563
+
564
+ [source,ruby]
565
+ ----
566
+ renderer.call "{% sample %}Your value is: <placeholder>.{% endsample %}"
567
+ # "Your value is: 85."
568
+ ----
569
+
570
+ With containers, you have the full capabilities of the {containable_link}. The above only scratches the surface of what's possible.
571
+
182
572
  == Development
183
573
 
184
574
  To contribute, run:
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "core"
4
+
5
+ module Superfluid
6
+ # The default configuration.
7
+ Configuration = Struct.new(
8
+ :default_resource_limits,
9
+ :error_mode,
10
+ :exception_renderer,
11
+ :file_system,
12
+ :filter_registry,
13
+ :tag_registry
14
+ ) do
15
+ def initialize(**)
16
+ super
17
+
18
+ self[:default_resource_limits] ||= Core::EMPTY_HASH
19
+ self[:error_mode] ||= :strict
20
+ self[:exception_renderer] ||= Core::Identity
21
+ self[:file_system] ||= Systems::Memory.new
22
+ self[:filter_registry] ||= Registries::Filter.new mode: error_mode
23
+ self[:tag_registry] ||= Registries::Tag.new
24
+ end
25
+ end
26
+ end
@@ -1,84 +1,82 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "core"
4
+ require "forwardable"
4
5
 
5
6
  module Superfluid
6
- # The default Liquid environment.
7
- Environment = Struct.new(
8
- :default_resource_limits,
9
- :error_mode,
10
- :exception_renderer,
11
- :file_system,
12
- :filter_registry,
13
- :tags
14
- ) do
15
- def self.for(**)
16
- instance = new(**)
17
-
18
- yield instance if block_given?
19
-
20
- instance.freeze
21
- end
7
+ # The default environment.
8
+ class Environment
9
+ extend Forwardable
22
10
 
23
11
  def self.default
24
12
  @default ||= new
25
13
  end
26
14
 
27
- def initialize(**)
28
- super
15
+ delegate Configuration.members => :configuration
16
+ delegate Configuration.members.map { :"#{it}=" } => :configuration
29
17
 
30
- self[:default_resource_limits] ||= Core::EMPTY_HASH
31
- self[:error_mode] ||= :strict
32
- self[:exception_renderer] ||= Core::Identity
33
- self[:file_system] ||= Systems::Memory.new
34
- self[:filter_registry] ||= Registries::Filter.for mode: error_mode
35
- self[:tags] ||= Liquid::Tags::STANDARD_TAGS.dup
18
+ def self.for(configuration = Configuration, **, &) = new(configuration[**], &)
36
19
 
37
- @filters_cache = {}
38
- end
20
+ def initialize configuration = Configuration.new
21
+ @configuration = configuration
39
22
 
40
- def create_filter_registry _context = nil,
41
- filters = Core::EMPTY_ARRAY,
42
- namespace: Liquid::StandardFilters
43
- return filter_registry if filters.empty?
23
+ yield self if block_given?
24
+ end
44
25
 
45
- filter_registry.add(namespace, *filters)
26
+ def create_filter_registry _context = nil, filters = Core::EMPTY_ARRAY
27
+ filters.empty? ? filter_registry : filter_registry.add(*filters)
46
28
  end
47
29
 
48
- alias_method :create_strainer, :create_filter_registry
30
+ alias create_strainer create_filter_registry
49
31
 
50
- def filter_names = filter_registry.names
32
+ def filter_names = configuration.filter_registry.names
51
33
 
52
- alias_method :filter_method_names, :filter_names
34
+ alias filter_method_names filter_names
53
35
 
54
- def register_filter(namespace, **)
55
- filters_cache.clear
56
- filter_registry.add(namespace, **)
36
+ def merge_filters(container, *)
37
+ filter_registry.merge(container, *)
38
+ self
39
+ end
40
+
41
+ def merge_tags(container, *)
42
+ tag_registry.merge(container, *)
43
+ self
44
+ end
45
+
46
+ def register_filter(*, **)
47
+ filter_registry.add(*, **)
57
48
  self
58
49
  end
59
50
 
60
51
  def register_filters(*, **)
61
- filters_cache.clear
62
52
  filter_registry.add(*, **)
63
53
  self
64
54
  end
65
55
 
66
56
  def register_tag name, klass
67
- tags[name.to_s] = klass
57
+ tag_registry.add name => klass
58
+ self
59
+ end
60
+
61
+ def register_tags(**)
62
+ tag_registry.add(**)
68
63
  self
69
64
  end
70
65
 
71
66
  def find_tag(name) = tags[name.to_s]
72
67
 
73
- alias_method :tag_for_name, :find_tag
68
+ alias tag_for_name find_tag
69
+
70
+ def tags = tag_registry.to_h
74
71
 
75
72
  def freeze
76
- tags.freeze
73
+ filter_registry.freeze
74
+ tag_registry.freeze
77
75
  super
78
76
  end
79
77
 
80
78
  private
81
79
 
82
- attr_reader :filters_cache
80
+ attr_reader :configuration
83
81
  end
84
82
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "containable"
4
+ require "core"
5
+ require "json"
6
+ require "refinements/string"
7
+
8
+ module Superfluid
9
+ module Filters
10
+ # A container of filter dependencies.
11
+ module Container
12
+ extend Containable
13
+
14
+ using Refinements::String
15
+
16
+ register(:jsonify) { |value| JSON.generate value }
17
+ register(:parse_json) { |value| JSON.parse value }
18
+
19
+ register :pluralize do |value, count = 0, suffix = "s", replace = /$/|
20
+ "#{count} #{value.pluralize suffix, count, replace:}"
21
+ end
22
+
23
+ register :singularize do |value, count = 0, suffix = "s", replace = Core::EMPTY_STRING|
24
+ "#{count} #{value.singularize suffix, count, replace:}"
25
+ end
26
+
27
+ register :trim_end do |value, maximum, trailer = "..."|
28
+ value.trim_end maximum, nil, trailer:
29
+ end
30
+
31
+ register(:trim_middle) { |value, maximum, gap = "..."| value.trim_middle maximum, gap: }
32
+ end
33
+ end
34
+ end
@@ -1,13 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "core"
4
+ require "functionable"
5
+
3
6
  module Superfluid
4
7
  module Registries
5
- # A filter registry.
6
- Filter = Data.define :mode, :commands do
7
- def self.for(namespace = Liquid::StandardFilters, mode: :strict) = new(mode:).add namespace
8
+ # The default filter registry.
9
+ class Filter
10
+ attr_reader :mode
11
+
12
+ def initialize container = Filters::Container,
13
+ defaults: Liquid::StandardFilters,
14
+ mode: :strict
15
+ @container = container
16
+ @mode = mode
8
17
 
9
- def initialize mode: :strict, commands: {}
10
- super
18
+ add defaults
11
19
  end
12
20
 
13
21
  def add(*, **)
@@ -16,61 +24,74 @@ module Superfluid
16
24
  self
17
25
  end
18
26
 
27
+ def merge(other, *)
28
+ container.merge(other, *)
29
+ self
30
+ end
31
+
19
32
  def call(name, *)
20
33
  dispatch(name, *)
21
34
  rescue ArgumentError => error
22
35
  raise Liquid::ArgumentError, error.message, error.backtrace
23
36
  end
24
37
 
25
- alias_method :invoke, :call
26
-
27
- def clear
28
- commands.clear
29
- self
30
- end
38
+ alias invoke call
31
39
 
32
- def names = commands.keys
40
+ def names = container.keys
33
41
 
34
42
  private
35
43
 
36
- def add_namespaces(*all) = all.each { add_methods it }
44
+ attr_reader :container
45
+
46
+ def add_namespaces(*collection) = collection.each { add_methods it }
37
47
 
38
48
  def add_methods namespace
49
+ # Order matters.
50
+ case namespace
51
+ in Class
52
+ fail Liquid::ArgumentError,
53
+ "Invalid type (#{namespace}) for namespace. Must be a Module or Functionable."
54
+ in Functionable then add_function_methods namespace
55
+ in Module then add_instance_methods namespace
56
+ # simplecov:disable
57
+ end
58
+ end
59
+
60
+ def add_function_methods namespace
61
+ namespace.function_methods
62
+ .each
63
+ .with_object({}) { |name, all| all[name] = namespace.method name }
64
+ .then { add_commands(**it) }
65
+ end
66
+
67
+ def add_instance_methods namespace
39
68
  shadow = Class.new.include(namespace).new
40
69
 
41
- all = namespace.instance_methods
42
- .each
43
- .with_object({}) do |method, all|
44
- name = method.name
45
- all[name] = shadow.method name
46
- end
70
+ collection = namespace.instance_methods
71
+ .each
72
+ .with_object({}) do |method, all|
73
+ name = method.name
74
+ all[name] = shadow.method name
75
+ end
47
76
 
48
- add_commands(**all)
77
+ add_commands(**collection)
49
78
  end
50
79
 
51
- def add_commands(**all) = all.each { |name, command| add_command name, command }
80
+ def add_commands(**collection) = collection.each { |name, command| add_command name, command }
52
81
 
53
- # :reek:ManualDispatch
54
- def add_command name, function
55
- key = name.to_s
82
+ def add_command name, callable
83
+ return if container.key? name
56
84
 
57
- return if commands.key? key
58
-
59
- case function
60
- in Proc | Method | Object if function.respond_to? :call then commands[key] = function
85
+ case callable
86
+ in Proc | Method | Core::Composable then container.register(name) { callable }
61
87
  else fail Liquid::ArgumentError, "Filter must be callable."
62
88
  end
63
89
  end
64
90
 
65
- def dispatch name, *positionals
66
- key = name.to_s
67
-
68
- if commands.key? key
69
- commands[key].call(*positionals)
70
- elsif mode == :strict
71
- fail Liquid::UndefinedFilter, "Undefined filter: #{name}."
72
- else
73
- positionals.first
91
+ def dispatch name, *arguments
92
+ if container.key? name then container[name].call(*arguments)
93
+ elsif mode == :strict then fail Liquid::UndefinedFilter, "Undefined filter: #{name}."
94
+ else arguments.first
74
95
  end
75
96
  end
76
97
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Superfluid
4
+ module Registries
5
+ # The default tag registry.
6
+ class Tag
7
+ def initialize container = Tags::Container, defaults: Liquid::Tags::STANDARD_TAGS
8
+ @container = container
9
+
10
+ add(**defaults)
11
+ end
12
+
13
+ def add **collection
14
+ collection.each { |name, tag| container.register name, tag unless container.key? name }
15
+ self
16
+ end
17
+
18
+ def merge(other, *)
19
+ container.merge(other, *)
20
+ self
21
+ end
22
+
23
+ def get(name) = (container[name] if container.key? name)
24
+
25
+ def names = container.keys
26
+
27
+ def to_h = container.each.to_h
28
+
29
+ private
30
+
31
+ attr_reader :container
32
+ end
33
+ end
34
+ end
@@ -1,14 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "core"
4
+
3
5
  module Superfluid
4
6
  # Renders template and associated data.
5
7
  class Renderer
6
- def initialize environment: Environment.for, parser: Liquid::Template
8
+ def initialize environment: Environment.new, parser: Liquid::Template
7
9
  @environment = environment
8
10
  @parser = parser
11
+
12
+ yield environment if block_given?
13
+
14
+ environment.freeze
9
15
  end
10
16
 
11
- def call(template, data) = parser.parse(template, environment:).render data
17
+ def call(template, data = Core::EMPTY_HASH) = parser.parse(template, environment:).render data
12
18
 
13
19
  private
14
20
 
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "containable"
4
+
5
+ module Superfluid
6
+ module Tags
7
+ # A container of tag dependencies.
8
+ module Container
9
+ extend Containable
10
+
11
+ register :template, Template
12
+ end
13
+ end
14
+ end
data/lib/superfluid.rb CHANGED
@@ -19,5 +19,5 @@ module Superfluid
19
19
 
20
20
  def self.build(**) = Environment.for(**) { yield it if block_given? }
21
21
 
22
- def self.new(**) = Renderer.new(**)
22
+ def self.new(**, &) = Renderer.new(**, &)
23
23
  end
data/superfluid.gemspec CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |spec|
4
4
  spec.name = "superfluid"
5
- spec.version = "0.0.0"
5
+ spec.version = "0.1.0"
6
6
  spec.authors = ["Brooke Kuhlmann"]
7
7
  spec.email = ["brooke@alchemists.io"]
8
8
  spec.homepage = "https://alchemists.io/projects/superfluid"
@@ -25,7 +25,9 @@ Gem::Specification.new do |spec|
25
25
  spec.required_ruby_version = ">= 4.0"
26
26
 
27
27
  spec.add_dependency "base64", "~> 0.3"
28
- spec.add_dependency "core", "~> 3.3"
28
+ spec.add_dependency "containable", "~> 2.5"
29
+ spec.add_dependency "core", "~> 3.4"
30
+ spec.add_dependency "functionable", "~> 1.4"
29
31
  spec.add_dependency "liquid", "~> 5.13"
30
32
  spec.add_dependency "refinements", "~> 14.0"
31
33
  spec.add_dependency "zeitwerk", "~> 2.8"
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: superfluid
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brooke Kuhlmann
@@ -49,20 +49,48 @@ dependencies:
49
49
  - - "~>"
50
50
  - !ruby/object:Gem::Version
51
51
  version: '0.3'
52
+ - !ruby/object:Gem::Dependency
53
+ name: containable
54
+ requirement: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - "~>"
57
+ - !ruby/object:Gem::Version
58
+ version: '2.5'
59
+ type: :runtime
60
+ prerelease: false
61
+ version_requirements: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - "~>"
64
+ - !ruby/object:Gem::Version
65
+ version: '2.5'
52
66
  - !ruby/object:Gem::Dependency
53
67
  name: core
54
68
  requirement: !ruby/object:Gem::Requirement
55
69
  requirements:
56
70
  - - "~>"
57
71
  - !ruby/object:Gem::Version
58
- version: '3.3'
72
+ version: '3.4'
73
+ type: :runtime
74
+ prerelease: false
75
+ version_requirements: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - "~>"
78
+ - !ruby/object:Gem::Version
79
+ version: '3.4'
80
+ - !ruby/object:Gem::Dependency
81
+ name: functionable
82
+ requirement: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - "~>"
85
+ - !ruby/object:Gem::Version
86
+ version: '1.4'
59
87
  type: :runtime
60
88
  prerelease: false
61
89
  version_requirements: !ruby/object:Gem::Requirement
62
90
  requirements:
63
91
  - - "~>"
64
92
  - !ruby/object:Gem::Version
65
- version: '3.3'
93
+ version: '1.4'
66
94
  - !ruby/object:Gem::Dependency
67
95
  name: liquid
68
96
  requirement: !ruby/object:Gem::Requirement
@@ -116,10 +144,14 @@ files:
116
144
  - LICENSE.adoc
117
145
  - README.adoc
118
146
  - lib/superfluid.rb
147
+ - lib/superfluid/configuration.rb
119
148
  - lib/superfluid/environment.rb
149
+ - lib/superfluid/filters/container.rb
120
150
  - lib/superfluid/registries/filter.rb
151
+ - lib/superfluid/registries/tag.rb
121
152
  - lib/superfluid/renderer.rb
122
153
  - lib/superfluid/systems/memory.rb
154
+ - lib/superfluid/tags/container.rb
123
155
  - lib/superfluid/tags/template.rb
124
156
  - superfluid.gemspec
125
157
  homepage: https://alchemists.io/projects/superfluid
@@ -147,7 +179,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
147
179
  - !ruby/object:Gem::Version
148
180
  version: '0'
149
181
  requirements: []
150
- rubygems_version: 4.0.19
182
+ rubygems_version: 4.0.20
151
183
  specification_version: 4
152
184
  summary: Enhances Liquid with object composition and functional design.
153
185
  test_files: []
metadata.gz.sig CHANGED
Binary file