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