You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

178 lines
6.2 KiB

# lib/questions_crafter/dsl.rb
module QuestionsCrafter
module Dsl
def self.included(base)
base.class_eval do
include ActiveModel::Model
include ActiveModel::Attributes if defined?(ActiveModel::Attributes)
include ActiveModel::Dirty
extend ClassMethods
# Add instance method to access class-level questions
define_method :questions do
self.class.questions
end
# Add method to get question attributes with values
define_method :question_attributes do
result = {}
self.class.questions.each do |question|
name = question[:name]
result[name] = {
value: self.send(name),
**question
}
end
result
end
# Add instance method for normalizing checkbox values
define_method :normalize_checkbox_group_value do |value|
case value
when Array
value
when String
if value.start_with?("[") && value.end_with?("]")
JSON.parse(value) rescue []
else
value.present? ? [ value ] : []
end
when NilClass, FalseClass
[]
else
value.present? ? [ value ] : []
end
end
end
end
module ClassMethods
def questions
@questions ||= []
end
def question_names
@question_names ||= {}
end
def question(name, as:, title: nil, description: nil, options: {}, required: false)
type_map = {
string: QuestionsCrafter::Constants::STRING,
text: QuestionsCrafter::Constants::TEXT,
radio: QuestionsCrafter::Constants::RADIO,
boolean: QuestionsCrafter::Constants::BOOLEAN,
integer: QuestionsCrafter::Constants::INTEGER,
select: QuestionsCrafter::Constants::SELECT,
checkbox_group: QuestionsCrafter::Constants::CHECKBOX_GROUP
}
type = type_map[as]
raise NotImplementedError,
"Question type '#{as}' is not implemented. Supported types are: #{type_map.keys.inspect}" unless type
add_question(name, type: type, title: title, description: description, options: options, required: required)
end
def title(title = nil)
@title = title if title
@title || name.demodulize.titleize
end
def description(description = nil)
@description = description if description
@description || ""
end
def back_link(link = nil)
@back_link = link if link
@back_link || "#"
end
def custom_html_classes(custom_html_classes = nil)
@custom_html_classes = custom_html_classes if custom_html_classes
@custom_html_classes || {}
end
private
# options: Can be either a hash of options with value as a key and text as the visible text for the option
# or an array of strings where each string is a symbol and it gets translated using I18n.t
def add_question(name, type:, options: {}, title: nil, description: nil, required: false)
raise QuestionAlreadyDefined, "Question #{name} already defined" if question_names[name]
question_names[name] = true
title ||= name.titleize
question = {
name: name,
type: type,
options: options,
title: title,
description: description,
required: required
}
questions << question
configure_question(question: question)
# TODO: Along with the adding of each question we need to set validations and other ActiveModel stuff
end
def configure_question(question:)
name = question[:name]
type = question[:type]
options = question[:options]
required = question[:required]
create_attribute(name, type, options)
setup_validations(name: name, type: type, options: options, required: required)
end
def setup_validations(name:, type:, options: {}, required:)
class_name = self.class.name.underscore
if type == QuestionsCrafter::Constants::CHECKBOX_GROUP
setup_checkbox_group_validations(name, class_name, options, required)
else
validates name, presence: { message: I18n.t("questions.errors.#{class_name}.#{name}.blank", default: I18n.t("errors.messages.blank")) } if required
validates name, inclusion: { in: options.keys, message: I18n.t("questions.errors.#{class_name}.#{name}.inclusion", default: I18n.t("errors.messages.inclusion")) } if options.any?
end
end
def setup_checkbox_group_validations(name, class_name, options, required)
validate do |record|
values = record.normalize_checkbox_group_value(record.send(name))
if required && values.blank?
record.errors.add(name, I18n.t("questions.errors.#{class_name}.#{name}.blank",
default: I18n.t("errors.messages.blank")))
end
if values.any?
invalid_values = values.reject { |v| options.keys.include?(v) }
if invalid_values.any?
record.errors.add(name, I18n.t("questions.errors.#{class_name}.#{name}.inclusion",
default: I18n.t("errors.messages.inclusion")))
end
end
end
end
# As we've included ActiveModel::Attributes we can use the attribute method to define attributes
def create_attribute(name, type, options)
attr_options = {} # TODO: we have options such as: default: true
attribute name.to_sym, crafter_type_to_attribute_type(type), **attr_options
end
def crafter_type_to_attribute_type(type)
case type
when QuestionsCrafter::Constants::STRING, QuestionsCrafter::Constants::TEXT,
QuestionsCrafter::Constants::SELECT, QuestionsCrafter::Constants::CHECKBOX_GROUP,
QuestionsCrafter::Constants::RADIO
:string
when QuestionsCrafter::Constants::BOOLEAN
:boolean
when QuestionsCrafter::Constants::INTEGER
:integer
end
end
end
end
end