<commit_before>module Scheherazade
  class CharacterBuilder < Struct.new(:character)
    AUTO = 'Auto'  # Note: must be equal? to AUTO (i.e. same object), not just ==

    def initialize(character)
      super
      @chain = [character]
      while (c = story.characters[@chain.first])
        @chain.unshift c
      end
      @model = Story.to_model(@chain.first)
      raise NameError, "Character not defined: #{@chain.first}" unless @model
      story.send :building, @ar = @model.new
      @chain.each{|c| story.current[c] = @ar }
    end

    # builds a character, filling required attributes,
    # default attributes for this type of character and
    # the attributes given.
    #
    # If the built object is invalid, the attributes with
    # errors will also be filled.
    #
    # An invalid object may be returned. In particular, it is
    # possible that it is only invalid because an associated
    # model is not yet valid.
    #
    def build(attribute_list = nil)
      @seq = (story.counter[@model] += 1)
      lists = [required_attributes, *default_attribute_lists, attribute_list]
      lists.prepend [:created_at] if @ar.has_attribute?(:created_at)
      attribute_list = lists.map{|al| canonical(al)}.inject(:merge)
      log(:building, attribute_list)
      set_attributes(attribute_list)
      unless @ar.valid?
        attribute_list = canonical(@ar.errors.map{|attr, _| attr})
        log(:fixing_errors, attribute_list)
        set_attributes(attribute_list)
      end
      yield @ar if block_given?
      log(:final_value, @ar)
      @ar
    end

  private
    def log(action, *args)
      Scheherazade.log(action, @model, *args)
    end

    def required_attributes
      @model.validators.select{|v| v.is_a?(ActiveModel::Validations::PresenceValidator) && v.options.empty?}
            .flat_map(&:attributes)
    end

    def default_attribute_lists
      story.fill_attributes.values_at(*@chain)
    end

    def set_attributes(attributes)
      attributes.each do |attr, value|
        set_attribute(attr, value)
      end
    end

    def set_attribute(attribute, value = AUTO)
      if assoc = @model.reflect_on_association(attribute)
        # It's possible that build records are not yet valid, so we
        # can expect errors on associations. At least for these reasons:
        return if @ar.send(attribute).present?

        value = assoc.klass.name.underscore.to_sym if value == AUTO
        value = value_for_association(assoc, value)
      elsif value.equal?(AUTO)
        value = automatic_value_for_basic_attribute(attribute)
      end
      log :setting, attribute, value
      @ar.send("#{attribute}=", value)
    end

    def value_for_association(assoc, associated_character)
      log(:setting_assocation, associated_character)
      key = assoc.options[:as] || assoc.active_record.name.underscore.to_sym
      opts = {key => @ar} if [:has_many, :has_one].include? assoc.macro
      ar =  if associated_character.is_a?(Symbol)
              story.current[associated_character] || self.class.new(associated_character).build(opts)
            else
              associated_character
            end
      case assoc.macro
      when :belongs_to
        ar
      when :has_and_belongs_to_many
        [ar]
      when :has_one
        ar
      when :has_many
        if ar && ar.persisted?
          if ar.send(assoc.active_record.name.underscore) != @ar
            log :additional_character, assoc.name
            [self.class.new(associated_character).build(opts)]
          else
            [ar]
          end
        else
          [*ar]
        end
      end
    end

    def automatic_value_for_basic_attribute(attribute)
      seq_string = " {#{@seq}}" if @seq > 1
      case @model.columns_hash[attribute.to_s].try(:type)
      when :integer  then @seq
      when :float    then @seq
      when :decimal  then "#{@seq}.99"
      when :datetime then Time.now - 1.day + @seq
      when :date     then Date.today - 1.year + @seq
      when :string   then
        case attribute
        when :email
          "<EMAIL>"
        when :name, :title
          "Example #{@model.name.humanize}#{seq_string}"
        else "Some #{attribute}#{seq_string}"<commit_msg>Fix bugs so has_many accepts arrays<commit_after>module Scheherazade
  class CharacterBuilder < Struct.new(:character)
    AUTO = 'Auto'  # Note: must be equal? to AUTO (i.e. same object), not just ==

    def initialize(character)
      super
      @chain = [character]
      while (c = story.characters[@chain.first])
        @chain.unshift c
      end
      @model = Story.to_model(@chain.first)
      raise NameError, "Character not defined: #{@chain.first}" unless @model
      story.send :building, @ar = @model.new
      @chain.each{|c| story.current[c] = @ar }
    end

    # builds a character, filling required attributes,
    # default attributes for this type of character and
    # the attributes given.
    #
    # If the built object is invalid, the attributes with
    # errors will also be filled.
    #
    # An invalid object may be returned. In particular, it is
    # possible that it is only invalid because an associated
    # model is not yet valid.
    #
    def build(attribute_list = nil)
      @seq = (story.counter[@model] += 1)
      lists = [required_attributes, *default_attribute_lists, attribute_list]
      lists.prepend [:created_at] if @ar.has_attribute?(:created_at)
      attribute_list = lists.map{|al| canonical(al)}.inject(:merge)
      log(:building, attribute_list)
      set_attributes(attribute_list)
      unless @ar.valid?
        attribute_list = canonical(@ar.errors.map{|attr, _| attr})
        log(:fixing_errors, attribute_list)
        set_attributes(attribute_list)
      end
      yield @ar if block_given?
      log(:final_value, @ar)
      @ar
    end

  private
    def log(action, *args)
      Scheherazade.log(action, @model, *args)
    end

    def required_attributes
      @model.validators.select{|v| v.is_a?(ActiveModel::Validations::PresenceValidator) && v.options.empty?}
            .flat_map(&:attributes)
    end

    def default_attribute_lists
      story.fill_attributes.values_at(*@chain)
    end

    def set_attributes(attributes)
      attributes.each do |attr, value|
        set_attribute(attr, value)
      end
    end

    def set_attribute(attribute, value = AUTO)
      if assoc = @model.reflect_on_association(attribute)
        # It's possible that build records are not yet valid, so we
        # can expect errors on associations. At least for these reasons:
        return if @ar.send(attribute).present?

        value = assoc.klass.name.underscore.to_sym if value == AUTO
        value = value_for_association(assoc, value)
      elsif value.equal?(AUTO)
        value = automatic_value_for_basic_attribute(attribute)
      end
      log :setting, attribute, value
      @ar.send("#{attribute}=", value)
    end

    def value_for_association(assoc, associated_character)
      log(:setting_assocation, associated_character)
      key = assoc.options[:as] || assoc.active_record.name.underscore.to_sym
      opts = {key => @ar} if [:has_many, :has_one].include? assoc.macro
      ar =  if associated_character.is_a?(Symbol)
              story.current[associated_character] || self.class.new(associated_character).build(opts)
            else
              associated_character
            end
      case assoc.macro
      when :belongs_to
        ar
      when :has_and_belongs_to_many
        [ar]
      when :has_one
        ar
      when :has_many
        if ar.respond_to?(:persisted?) && ar.persisted?
          if ar.send(assoc.active_record.name.underscore) != @ar
            log :additional_character, assoc.name
            [self.class.new(associated_character).build(opts)]
          else
            [ar]
          end
        else
          [*ar]
        end
      end
    end

    def automatic_value_for_basic_attribute(attribute)
      seq_string = " {#{@seq}}" if @seq > 1
      case @model.columns_hash[attribute.to_s].try(:type)
      when :integer  then @seq
      when :float    then @seq
      when :decimal  then "#{@seq}.99"
      when :datetime then Time.now - 1.day + @seq
      when :date     then Date.today - 1.year + @seq
      when :string   then
        case attribute
        when :email
          "<EMAIL>"
        when :name, :title
          "Example #{@model.name.humanize}#{seq_string}"
        else "Some #{attribute}#{seq_string}"
{'fraction_non_alphanumeric': 0.10215878258817979, 'fraction_numerical': 0.00141559513979002, 'mean_word_length': 2.5076541166735624, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 9, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '350867', 'n_tokens_mistral': 2559, 'n_tokens_neox': 2512, 'n_words': 648}