forresty
7/12/2017 - 7:36 AM

androsphinx.rb

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