form_template.rb

  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