jimjh/genie-parser

View on GitHub
lib/spirit/render/templates/problem.rb

Summary

Maintainability
A
35 mins
Test Coverage
require 'active_support/core_ext/class/attribute'
require 'spirit/constants'

module Spirit

  module Render

    # Renders a single problem. This class doesn't do anything useful; use the
    # child classes (e.g. {Spirit::Render::Multi}) instead. Child classes should
    # override {#valid?}.
    class Problem < Template

      # Required key in YAML markup. Value indicates type of problem.
      FORMAT = 'format'

      # Required key in YAML markup. Value contains question body.
      QUESTION = 'question'

      # Required key in YAML markup. Value contains answers.
      ANSWER = 'answer'

      # Generated ID.
      ID = 'id'

      # Required keys.
      KEYS = [FORMAT, QUESTION, ANSWER]

      # Stateless markdown renderer.
      class_attribute :markdown
      self.markdown = ::Redcarpet::Markdown.new(::Redcarpet::Render::HTML, MARKDOWN_EXTENSIONS)

      class << self

        # Parses the given text for questions and answers. If the given text
        # does not contain valid YAML or does not contain the format key, raises
        # an {Spirit::Render::RenderError}.
        # @param  [String]  text          embedded yaml
        # @return [Problem] problem
        def parse(text)
          digest = OpenSSL::Digest::SHA256.digest text
          yaml   = YAML.load text
          get_instance(yaml, digest)
        rescue ::Psych::SyntaxError => e
          raise RenderError, e.message
        end

        # Dynamically creates accessor methods for YAML values.
        # @example
        #   accessor :id
        def accessor(*args)
          args.each { |arg| define_method(arg) { @yaml[arg] } }
        end

        # @return [Problem] problem
        def get_instance(hash, digest)
          raise RenderError, "Missing 'format' key in given YAML" unless instantiable? hash
          klass = Spirit::Render.const_get(hash[FORMAT].capitalize)
          raise NameError unless klass < Problem
          klass.new(hash, digest)
        rescue NameError
          raise RenderError, 'Unrecognized format: %p' % hash[FORMAT]
        end

        private

        def instantiable?(hash)
          hash.is_a?(Hash) and hash.has_key?(FORMAT)
        end

      end

      accessor *KEYS
      attr_accessor :nesting, :id
      attr_reader   :digest

      # Creates a new problem from the given YAML.
      # @param [Hash] yaml          parsed yaml object
      # @param [String] digest      SHA-256 hash of yaml text
      # @param [Fixnum] id          integer ID of question
      def initialize(yaml, digest)
        @yaml, @digest, @nesting, @id = yaml, digest, [], 0
        raise RenderError.new('Invalid problem.') unless @yaml[QUESTION].is_a? String
        @yaml[QUESTION] = markdown.render @yaml[QUESTION]
      end

      # @todo TODO should probably show some error message in the preview,
      #   so that the author doesn't have to read the logs.
      def render(locals={})
        raise RenderError.new('Invalid problem.') unless valid?
        super @yaml.merge(locals)
      end

      # Retrieves the answer from the given YAML object in a serializable form.
      # @see #serializable
      # @return [String, Numeric, TrueClass, FalseClass] answer
      def answer
        serialize @yaml[ANSWER]
      end

      private

      # If +obj+ is one of String, Numeric, +true+, or +false+, the it's
      # returned. Otherwise, +to_s+ is invoked on the object and returned.
      # @return [String, Numeric, TrueClass, FalseClass] answer
      def serialize(obj)
        case obj
        when String, Numeric, TrueClass, FalseClass then obj
        else obj.to_s end
      end

      # Checks that all required {KEYS} exist in the YAML, and that the
      # question is given as a string.
      # @return [Boolean] true iff the parsed yaml contains a valid problem.
      def valid?
        KEYS.all? { |key| @yaml.has_key? key } and question.is_a? String
      end

    end

  end

end