module Androsphinx
class Node
# 最基础的树节点
def initialize(name)
@name = name
@childrens = []
end
def add_child(child)
@childrens << child
end
end
class Questionnaire < Node
# Questionnaire has_many sections
def initialize(name)
super
@next_section_id = 'A'
end
def add_child(section)
@childrens << section
section.id = @next_section_id
@next_section_id = @next_section_id.next
end
alias_method :add_section, :add_child
def to_h
{
name: @name,
sections: @childrens.map(&:to_h)
}
end
end
class QuestionGroup < Node
# 是一个抽象的问题组合
attr_accessor :id
alias_method :add_question, :add_child
def depends_on(choice)
@depends_on = choice
end
def to_h
hash = {
id: @id,
depends_on: @depends_on,
name: @name,
type: self.class.name.split('::').last,
questions: @childrens.map(&:to_h)
}
hash.reject { |_, v| v.nil? }
end
end
class Section < QuestionGroup
# section 是二级元素
def initialize(name)
super
@next_question_id = 1
end
def add_child(q)
super
q.id = "#{@id}#{@next_question_id}"
@next_question_id = @next_question_id.next
end
alias_method :add_question, :add_child
def to_h
{
section_id: @id,
name: @name,
questions: @childrens.map(&:to_h)
}
end
end
class RepeatedGroup < QuestionGroup
end
class Question < Node
attr_accessor :id
def to_h
hash = {
id: @id,
name: @name,
type: self.class.name.split('::').last
}
hash.reject { |_, v| v.nil? }
end
end
class FormattedQuestion < Question
attr_accessor :validations
def initialize(name, format, validations = {})
super(name)
@format = format
@validations = validations
end
def to_h
super.merge(format: @format, validations: @validations)
end
end
class ConditionalRepeatedGroupQuestion < FormattedQuestion
alias_method :add_question, :add_child
def to_h
super.merge(questions: @childrens.map(&:to_h))
end
end
class SingleChoiceQuestion < Question
attr_accessor :options
def initialize(name, options)
super(name)
@options = options
end
def to_h
if @childrens.empty?
super.merge(options: @options)
else
super.merge(options: @options)
.merge(questions: @childrens.map(&:to_h))
end
end
end
class MultipleChoicesQuestion < SingleChoiceQuestion
end
class DateQuestion < Question
end
class DSLContext
def self.execute(subject, block)
context = new(subject)
context.instance_exec(subject, &block)
end
private
def initialize(subject)
@subject = subject
end
def section(name, &block)
s = Section.new(name)
@subject.add_section(s)
self.class.execute(s, block) if block_given?
end
def question(name)
q = FormattedQuestion.new(name, '__')
@subject.add_question(q)
end
def question_group(name, &block)
group = QuestionGroup.new(name)
@subject.add_child(group)
self.class.execute(group, block)
end
def date_question(name = '时间')
q = DateQuestion.new(name)
@subject.add_question(q)
end
module ChoiceQuestionExtension
def options(choices)
@subject.options = choices
end
def selected(option, &block)
raise 'require a block' unless block_given?
unless @subject.is_a?(SingleChoiceQuestion) || @subject.is_a?(MultipleChoicesQuestion)
raise "require SingleChoiceQuestion or MultipleChoicesQuestion, got #{@subject}"
end
# 一般情况下 @subject 现在是一个 SingleChoiceQuestion
group = QuestionGroup.new(nil)
group.depends_on(option)
@subject.add_child(group)
self.class.execute(group, block)
end
def single_choice_question(name, options = nil, &block)
_choice_question(SingleChoiceQuestion, name, options, &block)
end
def multiple_choices_question(name, options = nil, &block)
_choice_question(MultipleChoicesQuestion, name, options, &block)
end
private
def _choice_question(klass, name, options = nil, &block)
q = klass.new(name, options)
@subject.add_question(q)
if block_given?
result = self.class.execute(q, block)
# 特殊处理:
# 如果 block 返回 array of strings, 认为这个 array 是 options
if result.is_a?(Array) &&
result.size > 0 &&
result.all? { |e| e.is_a?(String) } &&
q.options.nil?
q.options = result
end
end
raise '缺少 options' unless q.options && q.options.size > 0
end
end
include ChoiceQuestionExtension
module FormattedQuestionExtension
def formatted_question(*args)
name, format, validations = _extract_args(args)
q = FormattedQuestion.new(name, format, validations)
@subject.add_question(q)
end
private
def _extract_args(args)
case args.size
when 1
name, format, validations = nil, args.first, nil
when 2
if args.last.is_a?(Symbol)
name, format, validations = nil, args.first, args.last
else
name, format, validations = args.first, args.last, nil
end
when 3
name, format, validations = args
else
raise "不支持的 args: #{args}"
end
if validations.is_a?(Symbol)
# shorthand form for:
#
# formatted_question('身高', '__cm', :number)
case validations
when :number
validations = { type: validations, minimum: 0.1 }
when :integer
validations = { type: validations, minimum: 1 }
else
validations = { type: validations }
end
end
# replace ______cm with __cm
format = format.gsub(/_{2,}/, '__')
[name, format, validations]
end
end
include FormattedQuestionExtension
module RepeatedGroupExtension
def repeated_group(name = nil, &block)
group = RepeatedGroup.new(name)
@subject.add_child(group)
self.class.execute(group, block)
end
def repeat_group_with_question(*args, &block)
name, format, validations = _extract_args(args)
q = ConditionalRepeatedGroupQuestion.new(name, format, validations)
@subject.add_question(q)
self.class.execute(q, block) if block_given?
end
end
include RepeatedGroupExtension
module BooleanQuestionExtension
def boolean_question(name, &block)
single_choice_question(name, %w{ 否 是 }, &block)
end
def truthy(&block)
selected('是', &block)
end
end
include BooleanQuestionExtension
module PolarQuestionExtension
def polar_question(name, &block)
single_choice_question(name, %w{ (+) (-) }, &block)
end
def positive(&block)
selected('(+)', &block)
end
end
include PolarQuestionExtension
module PresenceQuestionExtension
def presence_question(name, &block)
single_choice_question(name, %w{ 无 有 }, &block)
end
def present(&block)
selected('有', &block)
end
end
include PresenceQuestionExtension
end
module DSL
def questionnaire(name, &block)
q = Questionnaire.new(name)
DSLContext.execute(q, block) if block_given?
q
end
end
end