liquidum 1.2.1 → 1.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.
data/config/routes.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  Liquidum::Engine.routes.draw do
2
- resources :completions
2
+ resources :variables, only: :index
3
3
  resources :conversions
4
4
  resources :sandbox
5
5
  post 'render_only', to: 'sandbox#render_only'
@@ -38,6 +38,9 @@ module Liquidum
38
38
 
39
39
  option :logger, default: -> { Logger.new($stdout).tap { |l| l.level = Logger::INFO } }, proc: true
40
40
  option :liquidum_file_system, default: "Liquidum::LiquidumFileSystem"
41
+ # The controller Liquidum's controllers inherit from, to decide who may
42
+ # use them, e.g. "Admin::BaseController". Set it in an initializer.
43
+ option :parent_controller, default: "::ApplicationController"
41
44
  attr_writer :i18n_store
42
45
 
43
46
  def initialize
data/lib/liquidum/drop.rb CHANGED
@@ -29,12 +29,12 @@ module Liquidum
29
29
  result = send(pm)
30
30
 
31
31
  h[pm] = if result.is_a? ApplicationDrop
32
- result.to_h(max_level, current_level: current_level + 1)
33
- elsif result.is_a? Enumerator::Lazy
34
- result.map { |r| r.to_h(max_level, current_level: current_level + 1) }.force
35
- else
36
- result
37
- end
32
+ result.to_h(max_level, current_level: current_level + 1)
33
+ elsif result.is_a? Enumerator::Lazy
34
+ result.map { |r| r.to_h(max_level, current_level: current_level + 1) }.force
35
+ else
36
+ result
37
+ end
38
38
  end
39
39
  h
40
40
  end
@@ -42,6 +42,8 @@ module Liquidum
42
42
  class << self
43
43
  def association(*associations)
44
44
  associations.each do |name|
45
+ self.associations << name.to_s
46
+
45
47
  define_method(name) do
46
48
  if @object.class.respond_to?(:reflect_on_all_associations)
47
49
  relation = @object.class.reflect_on_all_associations.find do |a|
@@ -50,7 +52,7 @@ module Liquidum
50
52
  end
51
53
  return unless relation
52
54
 
53
- klass = "#{relation.class_name}Drop".classify.safe_constantize
55
+ klass = Drop.for_relation(relation)
54
56
 
55
57
  Rails.logger.warn "No #{klass}" and return unless klass
56
58
 
@@ -64,6 +66,81 @@ module Liquidum
64
66
  end
65
67
  end
66
68
  end
69
+
70
+ # Adds to what Liquidum works out about a variable for the editor's
71
+ # variable picker (see .variables):
72
+ #
73
+ # describe :total, type: :money, hint: "Including VAT"
74
+ # describe :portal_url, label: -> { I18n.t("orders.portal_url") }
75
+ # describe :owner, drop: "UserDrop"
76
+ # describe :tags, drop: "TagDrop", collection: true
77
+ #
78
+ # A label or hint can be a proc, to translate it in the current locale.
79
+ def describe(name, label: nil, type: nil, hint: nil, drop: nil, collection: nil)
80
+ descriptions[name.to_s] = {label: label, type: type, hint: hint, drop: drop, collection: collection}.compact
81
+ end
82
+
83
+ def descriptions
84
+ @descriptions ||= superclass.respond_to?(:descriptions) ? superclass.descriptions.dup : {}
85
+ end
86
+
87
+ def associations
88
+ @associations ||= superclass.respond_to?(:associations) ? superclass.associations.dup : []
89
+ end
90
+
91
+ # The model the drop is for, for labels, types and associations:
92
+ # OrderDrop is for Order. Override it when the names don't match.
93
+ def model
94
+ name&.delete_suffix("Drop")&.safe_constantize
95
+ end
96
+
97
+ # The variables a template can use on this drop, one level deep, with
98
+ # labels in the current locale. Only what Liquid can call is listed, so
99
+ # not the helpers every Liquidum::Drop has (to_gid, to_h, …).
100
+ def variables
101
+ (invokable_methods - HELPERS).sort.filter_map do |name|
102
+ next if instance_method(name).parameters.any? { |kind, _| kind == :req }
103
+
104
+ variable(name)
105
+ end
106
+ end
107
+
108
+ def for_relation(relation)
109
+ "#{relation.class_name}Drop".classify.safe_constantize
110
+ end
111
+
112
+ private
113
+
114
+ def variable(name)
115
+ description = descriptions.fetch(name, {})
116
+ relation = model.reflect_on_association(name) if associations.include?(name) && model.respond_to?(:reflect_on_association)
117
+ drop = description[:drop]&.to_s&.safe_constantize || (for_relation(relation) if relation)
118
+
119
+ Variable.new(
120
+ name: name,
121
+ label: resolve(description[:label]) || label_for(name),
122
+ type: (description[:type] || (drop ? :object : attribute_type(name)))&.to_s,
123
+ hint: resolve(description[:hint]),
124
+ drop: drop,
125
+ collection: description.fetch(:collection) { relation&.collection? || false }
126
+ )
127
+ end
128
+
129
+ def label_for(name)
130
+ model.respond_to?(:human_attribute_name) ? model.human_attribute_name(name) : name.humanize
131
+ end
132
+
133
+ def attribute_type(name)
134
+ return unless model.respond_to?(:attribute_types) && model.attribute_types.key?(name)
135
+
136
+ model.attribute_types[name].type
137
+ end
138
+
139
+ def resolve(value)
140
+ value.respond_to?(:call) ? value.call : value
141
+ end
67
142
  end
143
+
144
+ HELPERS = (public_instance_methods(false).map(&:to_s) - %w[id]) + %w[to_liquid]
68
145
  end
69
146
  end
@@ -12,7 +12,7 @@ class RenderTag < LiquidumTag
12
12
  return unless argv1
13
13
 
14
14
  template = Liquid::Template.parse(argv1)
15
- template.render(context, registers: context.registers)
15
+ template.render(context)
16
16
  end
17
17
  end
18
18
 
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquidum
4
+ # A variable a template can use, as the editor's variable picker shows it.
5
+ # A variable with a drop has variables of its own; a collection is a list
6
+ # of them, to loop over.
7
+ Variable = Data.define(:name, :label, :type, :hint, :drop, :collection) do
8
+ def as_json(*)
9
+ {name: name, label: label, type: type, hint: hint, collection: collection, children: !drop.nil?}.compact
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquidum
4
+ # The variables available to a template, from its roots: the drops (or
5
+ # plain values) it's rendered with.
6
+ #
7
+ # variables = Liquidum::Variables.new(order: OrderDrop, portal_url: :string)
8
+ # variables.at("") # order, portal_url
9
+ # variables.at("order.customer") # the customer's variables
10
+ # variables.at("order.order_lines.product") # in a collection: an element's
11
+ #
12
+ # The editor asks for one level at a time (see Liquidum::VariablesController),
13
+ # so associations that lead back (order → customer → orders) are no problem.
14
+ # Hosts pass the roots signed, from .token, so a browser can't ask about
15
+ # other classes.
16
+ class Variables
17
+ attr_reader :roots
18
+
19
+ def initialize(roots)
20
+ @roots = roots.to_h { |name, root| [name.to_s, root] }
21
+ end
22
+
23
+ def at(path)
24
+ names = path.to_s.split(".")
25
+ return roots.map { |name, root| root_variable(name, root) } if names.empty?
26
+
27
+ first, *rest = names
28
+ variable = root_variable(first, roots[first]) if roots.key?(first)
29
+ rest.each { |name| variable = variable&.drop&.variables&.find { |v| v.name == name } }
30
+ variable&.drop&.variables
31
+ end
32
+
33
+ def token
34
+ self.class.verifier.generate(roots.transform_values { |root| root.is_a?(Class) ? {"drop" => root.name} : {"type" => root.to_s} })
35
+ end
36
+
37
+ class << self
38
+ def from_token(token)
39
+ roots = verifier.verified(token.to_s) or return
40
+
41
+ new(roots.transform_values { |root| root["drop"] ? root["drop"].safe_constantize : root["type"] })
42
+ end
43
+
44
+ def verifier
45
+ Rails.application.message_verifier("liquidum/variables")
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def root_variable(name, root)
52
+ if root.is_a?(Class) && root < Liquid::Drop
53
+ model = root.model if root.respond_to?(:model)
54
+ label = model.respond_to?(:model_name) ? model.model_name.human : name.humanize
55
+ Variable.new(name: name, label: label, type: "object", hint: nil, drop: (root if root.respond_to?(:variables)), collection: false)
56
+ else
57
+ Variable.new(name: name, label: name.humanize, type: root&.to_s, hint: nil, drop: nil, collection: false)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Liquidum
4
- VERSION = "1.2.1"
4
+ VERSION = "1.3.0"
5
5
  end
data/lib/liquidum.rb CHANGED
@@ -9,6 +9,8 @@ require "liquidum/version"
9
9
  require "liquidum/engine"
10
10
  require "liquidum/configuration"
11
11
  require "liquidum/drop"
12
+ require "liquidum/variable"
13
+ require "liquidum/variables"
12
14
 
13
15
  require "liquidum/liquid/liquid_helpers"
14
16
  require "liquidum/liquid/liquid_template_extensions"
@@ -47,8 +49,13 @@ module Liquidum
47
49
  options[:registers] ||= {}
48
50
  options[:registers]["file_system"] =
49
51
  Liquidum.config.liquidum_file_system.constantize.new(options[:registers])
50
- result = template.render(options[:context] || options[:assigns].stringify_keys,
51
- registers: options[:registers])
52
+ # A Liquid::Context (from a tag) already carries its registers, and since
53
+ # Liquid 5.4 those are a Liquid::Registers, which render won't take again
54
+ result = if options[:context]
55
+ template.render(options[:context])
56
+ else
57
+ template.render(options[:assigns].stringify_keys, registers: options[:registers])
58
+ end
52
59
 
53
60
  if template.errors.present?
54
61
  Liquidum.config.logger.error "--- Template rendering errors: " + "-" * 49
@@ -63,7 +70,7 @@ module Liquidum
63
70
 
64
71
  assigns = options[:assigns].deep_stringify_keys
65
72
  assigns = assigns.merge(template.assigns.stringify_keys) if template.assigns
66
- options[:registers].deep_merge!(template.registers.stringify_keys) if template.registers
73
+ template.registers&.each { |key, value| options[:registers][key.to_s] = value }
67
74
 
68
75
  if options[:filter].present? && Tilt[options[:filter]]
69
76
  result = Tilt[options[:filter]].new(options[:filter_options]) do
data/liquidum.gemspec CHANGED
@@ -28,7 +28,7 @@ Gem::Specification.new do |spec|
28
28
  spec.add_runtime_dependency("i18n", ["~> 1.8"])
29
29
  spec.add_runtime_dependency("kramdown", ["~> 2.1"])
30
30
  spec.add_runtime_dependency("kramdown-parser-gfm", ["~> 1.0"])
31
- spec.add_runtime_dependency("liquid", ["< 5.4"])
31
+ spec.add_runtime_dependency("liquid", ["< 6"])
32
32
  spec.add_runtime_dependency("parslet", [">= 0"])
33
33
  spec.add_runtime_dependency("pg", [">= 0"])
34
34
  spec.add_runtime_dependency("rails", [">= 5.2"])
data/mise.toml ADDED
@@ -0,0 +1,4 @@
1
+ # If gpg fails use `MISE_GPG_VERIFY=false`
2
+ [tools]
3
+ node = "24"
4
+ ruby = "4"
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: liquidum
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.1
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tom de Grunt
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-06-17 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: addressable
@@ -86,14 +85,14 @@ dependencies:
86
85
  requirements:
87
86
  - - "<"
88
87
  - !ruby/object:Gem::Version
89
- version: '5.4'
88
+ version: '6'
90
89
  type: :runtime
91
90
  prerelease: false
92
91
  version_requirements: !ruby/object:Gem::Requirement
93
92
  requirements:
94
93
  - - "<"
95
94
  - !ruby/object:Gem::Version
96
- version: '5.4'
95
+ version: '6'
97
96
  - !ruby/object:Gem::Dependency
98
97
  name: parslet
99
98
  requirement: !ruby/object:Gem::Requirement
@@ -269,18 +268,26 @@ executables: []
269
268
  extensions: []
270
269
  extra_rdoc_files: []
271
270
  files:
271
+ - ".devcontainer/devcontainer-lock.json"
272
+ - ".devcontainer/devcontainer.json"
273
+ - ".devcontainer/docker-compose.yml"
274
+ - ".devcontainer/env.conf"
275
+ - ".devcontainer/postStart.sh"
272
276
  - ".github/workflows/gem-push.yml"
273
277
  - ".gitignore"
274
278
  - ".rubocop.yml"
279
+ - ".shipkit.yml"
280
+ - ".worktreeinclude"
281
+ - CHANGELOG.md
275
282
  - Gemfile
276
283
  - MIT-LICENSE
277
284
  - README.md
278
285
  - Rakefile
279
286
  - TODO.md
280
287
  - app/controllers/liquidum/application_controller.rb
281
- - app/controllers/liquidum/completions_controller.rb
282
288
  - app/controllers/liquidum/conversions_controller.rb
283
289
  - app/controllers/liquidum/sandbox_controller.rb
290
+ - app/controllers/liquidum/variables_controller.rb
284
291
  - app/helpers/liquidum/application_helper.rb
285
292
  - app/jobs/liquidum/application_job.rb
286
293
  - app/models/concerns/liquidum/to_liquid.rb
@@ -290,6 +297,7 @@ files:
290
297
  - app/views/liquidum/conversions/index.html.slim
291
298
  - app/views/liquidum/sandbox/edit.html.slim
292
299
  - app/views/liquidum/sandbox/index.html.slim
300
+ - bin/de
293
301
  - bin/rails
294
302
  - bin/rubocop
295
303
  - config/routes.rb
@@ -322,13 +330,15 @@ files:
322
330
  - lib/liquidum/liquid/parser.rb
323
331
  - lib/liquidum/liquid/tags/helper_tag.rb
324
332
  - lib/liquidum/liquid/tags/render_tag.rb
333
+ - lib/liquidum/variable.rb
334
+ - lib/liquidum/variables.rb
325
335
  - lib/liquidum/version.rb
326
336
  - liquidum.gemspec
337
+ - mise.toml
327
338
  homepage: https://github.com/entdec/liquidum
328
339
  licenses:
329
340
  - MIT
330
341
  metadata: {}
331
- post_install_message:
332
342
  rdoc_options: []
333
343
  require_paths:
334
344
  - lib
@@ -343,8 +353,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
343
353
  - !ruby/object:Gem::Version
344
354
  version: '0'
345
355
  requirements: []
346
- rubygems_version: 3.4.10
347
- signing_key:
356
+ rubygems_version: 3.6.9
348
357
  specification_version: 4
349
358
  summary: Liquid templating - on steroids
350
359
  test_files: []
@@ -1,15 +0,0 @@
1
- require_dependency "liquidum/application_controller"
2
-
3
- module Liquidum
4
- class CompletionsController < ApplicationController
5
- def index
6
- q = params[:q]
7
- klass = q.classify.safe_constantize
8
- drop = q.classify.concat('Drop').safe_constantize
9
- invokables = drop&.invokable_methods || []
10
- # drop_relations = klass.reflect_on_all_associations.map(&:name).select {|n| invokables.include?(n.to_s) }
11
-
12
- render json: { q: q, completions: invokables }
13
- end
14
- end
15
- end