1# frozen_string_literal: true
2
3require "blather"
4
5class FormTemplate
6 def initialize(template, filename="template", **kwargs)
7 @args = kwargs
8 @template = template
9 @filename = filename
10 freeze
11 end
12
13 def self.for(path, **kwargs)
14 if path.is_a?(FormTemplate)
15 raise "Sent args and a FormTemplate" unless kwargs.empty?
16
17 return path
18 end
19
20 full_path = File.dirname(__dir__) + "/forms/#{path}.rb"
21 new(File.read(full_path), full_path, **kwargs)
22 end
23
24 def self.render(path, context=OneRender.new, **kwargs)
25 self.for(path).render(context, **kwargs)
26 end
27
28 def render(context=OneRender.new, **kwargs)
29 one = context.merge(**@args).merge(**kwargs)
30 one.instance_eval(@template, @filename)
31 one.form
32 end
33
34 class OneRender
35 def initialize(**kwargs)
36 kwargs.each do |k, v|
37 instance_variable_set("@#{k}", v)
38 end
39 @__form = Blather::Stanza::X.new
40 @__builder = Nokogiri::XML::Builder.with(@__form)
41 end
42
43 def merge(**kwargs)
44 OneRender.new(**to_h.merge(kwargs))
45 end
46
47 def form!
48 @__type_set = true
49 @__form.type = :form
50 end
51
52 def result!
53 @__type_set = true
54 @__form.type = :result
55 end
56
57 def title(s)
58 @__form.title = s
59 end
60
61 def instructions(s)
62 @__form.instructions = s
63 end
64
65 def validate(field, datatype: nil, **kwargs)
66 Nokogiri::XML::Builder.with(field) do |xml|
67 xml.validate(
68 xmlns: "http://jabber.org/protocol/xdata-validate",
69 datatype: datatype || "xs:string"
70 ) do
71 xml.basic unless validation_type(xml, **kwargs)
72 end
73 end
74 end
75
76 def validation_type(xml, open: false, regex: nil, range: nil)
77 xml.open if open
78 xml.range(min: range.first, max: range.last) if range
79 xml.regex(regex.source) if regex
80 open || regex || range
81 end
82
83 # Given a map of fields to labels, and a list of objects this will
84 # produce a table from calling each field's method on every object in the
85 # list. So, this list is value_semantics / OpenStruct style
86 def table(list, **fields)
87 keys = fields.keys
88 FormTable.new(
89 list.map { |x| keys.map { |k| x.public_send(k) } },
90 **fields
91 ).add_to_form(@__form)
92 end
93
94 def simple_child(field, name, xmlns, content)
95 return unless content
96
97 Nokogiri::XML::Builder.with(field) do |xml|
98 xml.public_send(name, content, xmlns: xmlns)
99 end
100 end
101
102 def field(
103 datatype: nil, open: false, regex: nil, range: nil,
104 suffix: nil, prefix: nil,
105 **kwargs
106 )
107 f = Blather::Stanza::X::Field.new(kwargs)
108 if datatype || open || regex || range
109 validate(f, datatype: datatype, open: open, regex: regex, range: range)
110 end
111 simple_child(f, :x, "https://ns.cheogram.com/suffix-label", suffix)
112 simple_child(f, :x, "https://ns.cheogram.com/prefix-label", prefix)
113 @__form.fields += [f]
114 end
115
116 def context
117 PartialRender.new(@__form, @__builder, **to_h)
118 end
119
120 def render(path, **kwargs)
121 FormTemplate.render(path, context, **kwargs)
122 end
123
124 def to_h
125 instance_variables
126 .reject { |sym| sym.to_s.start_with?("@__") }
127 .each_with_object({}) { |var, acc|
128 name = var.to_s[1..-1]
129 acc[name.to_sym] = instance_variable_get(var)
130 }
131 end
132
133 def xml
134 @__builder
135 end
136
137 def form
138 raise "Type never set" unless @__type_set
139
140 @__form
141 end
142
143 class PartialRender < OneRender
144 def initialize(form, builder, **kwargs)
145 kwargs.each do |k, v|
146 instance_variable_set("@#{k}", v)
147 end
148 @__form = form
149 @__builder = builder
150 end
151
152 def merge(**kwargs)
153 PartialRender.new(@__form, @__builder, **to_h.merge(kwargs))
154 end
155
156 # As a partial, we are not a complete form
157 def form; end
158
159 def form!
160 raise "Invalid 'form!' in Partial"
161 end
162
163 def result!
164 raise "Invalid 'result!' in Partial"
165 end
166
167 def title(_)
168 raise "Invalid 'title' in Partial"
169 end
170
171 def instructions(_)
172 raise "Invalid 'instructions' in Partial"
173 end
174 end
175 end
176end