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 field(datatype: nil, open: false, regex: nil, range: nil, **kwargs)
95 f = Blather::Stanza::X::Field.new(kwargs)
96 if datatype || open || regex || range
97 validate(f, datatype: datatype, open: open, regex: regex, range: range)
98 end
99 @__form.fields += [f]
100 end
101
102 def context
103 PartialRender.new(@__form, @__builder, **to_h)
104 end
105
106 def render(path, **kwargs)
107 FormTemplate.render(path, context, **kwargs)
108 end
109
110 def to_h
111 instance_variables
112 .reject { |sym| sym.to_s.start_with?("@__") }
113 .each_with_object({}) { |var, acc|
114 name = var.to_s[1..-1]
115 acc[name.to_sym] = instance_variable_get(var)
116 }
117 end
118
119 def xml
120 @__builder
121 end
122
123 def form
124 raise "Type never set" unless @__type_set
125
126 @__form
127 end
128
129 class PartialRender < OneRender
130 def initialize(form, builder, **kwargs)
131 kwargs.each do |k, v|
132 instance_variable_set("@#{k}", v)
133 end
134 @__form = form
135 @__builder = builder
136 end
137
138 def merge(**kwargs)
139 PartialRender.new(@__form, @__builder, **to_h.merge(kwargs))
140 end
141
142 # As a partial, we are not a complete form
143 def form; end
144
145 def form!
146 raise "Invalid 'form!' in Partial"
147 end
148
149 def result!
150 raise "Invalid 'result!' in Partial"
151 end
152
153 def title(_)
154 raise "Invalid 'title' in Partial"
155 end
156
157 def instructions(_)
158 raise "Invalid 'instructions' in Partial"
159 end
160 end
161 end
162end