af83/chouette-core

View on GitHub
app/models/macro/create_code.rb

Summary

Maintainability
A
55 mins
Test Coverage
module Macro
  class CreateCode < Base
    # Use enumerize directly
    enumerize :target_model, in: %w{StopArea Line VehicleJourney}

    option :target_model
    option :source_attribute # TODO use ModelAttribute ?
    option :source_pattern
    option :target_code_space# TODO must be id or short_name of one of Workgroup CodeSpaces
    option :target_pattern

    # Use standard Rails validation methods
    validates :target_model, :source_attribute, :target_code_space, presence: true

    class Run < Macro::Base::Run
      # TODO copy options from Macro::CreateCode class
      option :target_model
      option :source_attribute
      option :source_pattern
      option :target_code_space
      option :target_pattern

      def run
        # This Updater pattern made simple to test
        #
        # Could be optimize with a more complex logic:
        # - read all source value (with cursor)
        # - compute all target value
        # - create all required codes with inserter ?

        request = CreateCodeFromUuid::Run::RequestBuilder.new(workgroup, models, code_space, target_pattern).run
        request.find_in_batches do |batch|
          model_class.transaction do
            batch.each do |model|
              if source_value = source.value(model)
                code_value = target.value(model, source_value)
                code = model.codes.create(code_space: code_space, value: code_value)
                create_message(model, code, source_value)
              end
            end
          end
        end
      end

      # Create a message for the given Model
      # If the Model is invalid, an error message is created.
      def create_message(model, code, source_value)
        attributes = {
          message_attributes: {
            code_value: code.value,
            name: model.try(:name) || model.try(:published_name) || source_value
          },
          source: model
        }

        attributes.merge!(criticity: 'error', message_key: 'error') unless code.persisted?

        macro_messages.create!(attributes)
      end

      def source
        @source ||= Source.new(
          workgroup: workgroup,
          attribute: source_attribute,
          pattern: source_pattern,
        )
      end

      def target
        @target ||= Target.new(pattern: target_pattern)
      end

      def code_space
        @code_space ||= workgroup.code_spaces.find_by(short_name: target_code_space)
      end

      def model_class
        @model_class ||=
          "Chouette::#{target_model}".constantize rescue nil || target_model.constantize
      end

      def model_collection
        target_model.underscore.pluralize
      end

      def models
        @models ||= scope.send(model_collection)
      end
    end

    class Source
      attr_accessor :workgroup, :attribute, :pattern
      def initialize(attributes = {})
        attributes.each { |k,v| send "#{k}=", v }
      end

      def value(model)
        apply_pattern raw_value(model)
      end

      def raw_value(model)
        unless code_space
          model.send attribute
        else
          model.codes.find_by(code_space: code_space)&.value
        end
      end

      def apply_pattern(value)
        if pattern_regexp && pattern_regexp =~ value
          $1
        else
          value
        end
      end

      def pattern_regexp
        @pattern_regexp ||= Regexp.new(pattern) if pattern.present?
      end

      def code_space_short_name
        if /^code:(.*)/ =~ attribute
          $1
        end
      end

      def code_space
        return unless workgroup
        @code_space ||= workgroup.code_spaces.find_by(short_name: code_space_short_name)
      end
    end

    class Target
      attr_accessor :pattern
      def initialize(attributes = {})
        attributes.each { |k,v| send "#{k}=", v }
      end

      def value(model, value)
        apply_pattern(model, value)
      end

      def has_pattern?
        pattern.present?
      end

      def apply_pattern(model, value) # rubocop:disable Metrics/MethodLength
        return value unless has_pattern?

        result = pattern.gsub(VALUE_REGEXP) do
          if ::Regexp.last_match(1) && ::Regexp.last_match(2)
            from = ::Regexp.new(::Regexp.last_match(1))
            to = ::Regexp.last_match(2)
            value.gsub(from, to)
          else
            value
          end
        end
        result.gsub(CODE_SPACE_REGEXP) do
          if ::Regexp.last_match(1)
            model.send("line_code_#{::Regexp.last_match(1)}")
          else
            model.line_registration_number
          end
        end
      end
    end

    VALUE_REGEXP = %r@%{value(?://([^/]+)/([^}]*))?}@.freeze
    CODE_SPACE_REGEXP = /%{line.code(?::([^}]*))?}/.freeze
  end
end