# frozen_string_literal: true require "blather" class FormTemplate def initialize(template, filename="template", **kwargs) @args = kwargs @template = template @filename = filename freeze end def self.for(path, **kwargs) if path.is_a?(FormTemplate) raise "Sent args and a FormTemplate" unless kwargs.empty? return path end full_path = File.dirname(__dir__) + "/forms/#{path}.rb" new(File.read(full_path), full_path, **kwargs) end def self.render(path, context=OneRender.new, **kwargs) self.for(path).render(context, **kwargs) end def render(context=OneRender.new, **kwargs) one = context.merge(**@args).merge(**kwargs) one.instance_eval(@template, @filename) one.form end class OneRender def initialize(**kwargs) kwargs.each do |k, v| instance_variable_set("@#{k}", v) end @__form = Blather::Stanza::X.new @__builder = Nokogiri::XML::Builder.with(@__form) end def merge(**kwargs) OneRender.new(**to_h.merge(kwargs)) end def form! @__type_set = true @__form.type = :form end def result! @__type_set = true @__form.type = :result end def title(s) @__form.title = s end def instructions(s) @__form.instructions = s end def validate(field, datatype: nil, **kwargs) Nokogiri::XML::Builder.with(field) do |xml| xml.validate( xmlns: "http://jabber.org/protocol/xdata-validate", datatype: datatype || "xs:string" ) do xml.basic unless validation_type(xml, **kwargs) end end end def validation_type(xml, open: false, regex: nil, range: nil) xml.open if open xml.range(min: range.first, max: range.last) if range xml.regex(regex.source) if regex open || regex || range end # Given a map of fields to labels, and a list of objects this will # produce a table from calling each field's method on every object in the # list. So, this list is value_semantics / OpenStruct style def table(list, **fields) keys = fields.keys FormTable.new( list.map { |x| keys.map { |k| x.public_send(k) } }, **fields ).add_to_form(@__form) end def field(datatype: nil, open: false, regex: nil, range: nil, **kwargs) f = Blather::Stanza::X::Field.new(kwargs) if datatype || open || regex || range validate(f, datatype: datatype, open: open, regex: regex, range: range) end @__form.fields += [f] end def context PartialRender.new(@__form, @__builder, **to_h) end def render(path, **kwargs) FormTemplate.render(path, context, **kwargs) end def to_h instance_variables .reject { |sym| sym.to_s.start_with?("@__") } .each_with_object({}) { |var, acc| name = var.to_s[1..-1] acc[name.to_sym] = instance_variable_get(var) } end def xml @__builder end def form raise "Type never set" unless @__type_set @__form end class PartialRender < OneRender def initialize(form, builder, **kwargs) kwargs.each do |k, v| instance_variable_set("@#{k}", v) end @__form = form @__builder = builder end def merge(**kwargs) PartialRender.new(@__form, @__builder, **to_h.merge(kwargs)) end # As a partial, we are not a complete form def form; end def form! raise "Invalid 'form!' in Partial" end def result! raise "Invalid 'result!' in Partial" end def title(_) raise "Invalid 'title' in Partial" end def instructions(_) raise "Invalid 'instructions' in Partial" end end end end