<commit_before>class Experiments::Experiment

  include Experiments::Metadata

  attr_reader :handle, :qualifier, :subject_storage, :event_logger

  def self.define(handle, *args, &block)
    experiment = self.new(handle, *args, &block)
    raise Experiments::ExperimentHandleNotUnique.new(experiment.handle) if Experiments.repository.has_key?(experiment.handle)
    Experiments.repository[experiment.handle] = experiment
  end

  def initialize(handle, options = {}, &block)
    @handle = handle.to_s

    options = default_options.merge(options)
    @qualifier         = options[:qualifier]
    @event_logger      = options[:event_logger]
    @subject_storage   = options[:storage]
    @store_unqualified = options[:store_unqualified]
    @segmenter         = options[:segmenter]
    @subject_type      = options[:subject_type]
    instance_eval(&block) if block_given?
  end

  def subject_type(type = nil)
    return @subject_type if type.nil?
    @subject_type = type
  end

  def store_unqualified?
    @store_unqualified
  end

  def group(handle)
    segmenter.groups[handle.to_s]
  end

  def groups(segmenter_class = Experiments::Segmenter::StaticPercentage, &block)
    return @segmenter.groups unless block_given?
    @segmenter ||= segmenter_class.new(self)
    @segmenter.instance_eval(&block)
    @segmenter.verify!
    return self
  end

  def qualify(&block)
    @qualifier = block
  end

  def storage(subject_storage, options = {})
    @store_unqualified = options[:store_unqualified] if options.has_key?(:store_unqualified)
    @subject_storage = subject_storage
  end

  def segmenter
    raise Experiments::Error, "No groups defined for experiment #{@handle.inspect}." if @segmenter.nil?
    @segmenter
  end

  def group_handles
    segmenter.groups.keys
  end

  def subject_assignment(subject_identifier, group, returning = true)
    Experiments::Assignment.new(self, subject_identifier, group, returning)
  end

  def subject_conversion(subject_identifier, goal)
    Experiments::Conversion.new(self, subject_identifier, goal)
  end

  def convert(subject, goal)
    identifier = retrieve_subject_identifier(subject)
    conversion = subject_conversion(identifier, goal)
    event_logger.log_conversion(conversion)
    conversion
  end

  def assign(subject, context = nil)
    identifier = retrieve_subject_identifier(subject)
    assignment = if store_unqualified?
      assignment_with_unqualified_persistence(identifier, subject, context)
    else
      assignment_without_unqualified_persistence(identifier, subject, context)
    end

    @subject_storage.store_assignment(assignment) if should_store_assignment?(assignment)
    event_logger.log_assignment(assignment)
    assignment
  rescue Experiments::StorageError
    subject_assignment(identifier, nil, false)
  end

  def switch(subject, context = nil)
    assign(subject, context).to_sym
  end

  def retrieve_subject_identifier(subject)
    identifier = subject_identifier(subject).to_s
    raise Experiments::EmptySubjectIdentifier, "Subject resolved to an empty identifier!" if identifier.empty?
    identifier
  end

  def has_qualifier?
    !@qualifier.nil?
  end

  def everybody_qualifies?
    !has_qualifier?
  end

  def as_json(options = {})
    data = {
      handle: handle,
      has_qualifier: has_qualifier?,
      groups: segmenter.groups.values.map { |g| g.as_json(options) },
      metadata: metadata
    }

    data.tap do |data|
      data[:subject_type] = subject_type.to_s unless subject_type.nil?
    end
  end

  def to_json(options = {})
    as_json(options).to_json
  end 

  def fetch_subject(subject_identifier)
    raise NotImplementedError, "Fetching subjects based in identifier is not implemented for eperiment @{handle.inspect}."
  end

  def fetch_assignment(subject_identifier)
    @subject_storage.retrieve_assignment(self, subject_identifier)
  end

  protected

  def default_options
    {
      event_logger: Experiments::EventLogger.new(Experiments.default_logger),
      storage: Experiments::Storage::Dummy.new
    }
  end

  def should_store_assignment?(assignment)
    !assignment.returning? && (store_unqualified? || assignment.qualified?)
  end

  def assignment_with_unqualified_persistence(subject_identifier, subject, context)
    fetch_assignment(subject_identifier) || (
      subject_qualifies?(subject, context) ? 
        subject_assignment(subject_identifier, @segmenter.assign(subject_identifier, subject, context), false) :
        subject_assignment(subject_identifier, nil, false)
    )
  end

  def assignment_without_unqualified_persistence(subject_identifier, subject, context)
    if subject_qualifies?(subject, context)
      fetch_assignment(subject_identifier) ||
        subject_assignment(subject_identifier, @segmenter.assign(subject_identifier, subject, context), false)
    else 
      subject_assignment(subject_identifier, nil, false)
    end
  end  

  def subject_identifier(subject)
    subject.respond_to?(:id) ? subject.id : subject.to_s
  end

  def subject_qualifies?(subject, context = nil)
    everybody_qualifies? || @qualifier.call(subject, context)
  end
end
<commit_msg>Better handling of default experiment options.<commit_after>class Experiments::Experiment

  include Experiments::Metadata

  attr_reader :handle, :qualifier, :subject_storage, :event_logger

  def self.define(handle, *args, &block)
    experiment = self.new(handle, *args, &block)
    raise Experiments::ExperimentHandleNotUnique.new(experiment.handle) if Experiments.repository.has_key?(experiment.handle)
    Experiments.repository[experiment.handle] = experiment
  end

  def initialize(handle, options = {}, &block)
    @handle = handle.to_s

    options = default_options.merge(options)
    @qualifier         = options[:qualifier]
    @event_logger      = options[:event_logger] || Experiments::EventLogger.new(Experiments.default_logger)
    @subject_storage   = options[:storage] || Experiments::Storage::Dummy.new
    @store_unqualified = options[:store_unqualified]
    @segmenter         = options[:segmenter]
    @subject_type      = options[:subject_type]
    instance_eval(&block) if block_given?
  end

  def subject_type(type = nil)
    return @subject_type if type.nil?
    @subject_type = type
  end

  def store_unqualified?
    @store_unqualified
  end

  def group(handle)
    segmenter.groups[handle.to_s]
  end

  def groups(segmenter_class = Experiments::Segmenter::StaticPercentage, &block)
    return @segmenter.groups unless block_given?
    @segmenter ||= segmenter_class.new(self)
    @segmenter.instance_eval(&block)
    @segmenter.verify!
    return self
  end

  def qualify(&block)
    @qualifier = block
  end

  def storage(subject_storage, options = {})
    @store_unqualified = options[:store_unqualified] if options.has_key?(:store_unqualified)
    @subject_storage = subject_storage
  end

  def segmenter
    raise Experiments::Error, "No groups defined for experiment #{@handle.inspect}." if @segmenter.nil?
    @segmenter
  end

  def group_handles
    segmenter.groups.keys
  end

  def subject_assignment(subject_identifier, group, returning = true)
    Experiments::Assignment.new(self, subject_identifier, group, returning)
  end

  def subject_conversion(subject_identifier, goal)
    Experiments::Conversion.new(self, subject_identifier, goal)
  end

  def convert(subject, goal)
    identifier = retrieve_subject_identifier(subject)
    conversion = subject_conversion(identifier, goal)
    event_logger.log_conversion(conversion)
    conversion
  end

  def assign(subject, context = nil)
    identifier = retrieve_subject_identifier(subject)
    assignment = if store_unqualified?
      assignment_with_unqualified_persistence(identifier, subject, context)
    else
      assignment_without_unqualified_persistence(identifier, subject, context)
    end

    @subject_storage.store_assignment(assignment) if should_store_assignment?(assignment)
    event_logger.log_assignment(assignment)
    assignment
  rescue Experiments::StorageError
    subject_assignment(identifier, nil, false)
  end

  def switch(subject, context = nil)
    assign(subject, context).to_sym
  end

  def retrieve_subject_identifier(subject)
    identifier = subject_identifier(subject).to_s
    raise Experiments::EmptySubjectIdentifier, "Subject resolved to an empty identifier!" if identifier.empty?
    identifier
  end

  def has_qualifier?
    !@qualifier.nil?
  end

  def everybody_qualifies?
    !has_qualifier?
  end

  def as_json(options = {})
    data = {
      handle: handle,
      has_qualifier: has_qualifier?,
      groups: segmenter.groups.values.map { |g| g.as_json(options) },
      metadata: metadata
    }

    data.tap do |data|
      data[:subject_type] = subject_type.to_s unless subject_type.nil?
    end
  end

  def to_json(options = {})
    as_json(options).to_json
  end 

  def fetch_subject(subject_identifier)
    raise NotImplementedError, "Fetching subjects based in identifier is not implemented for eperiment @{handle.inspect}."
  end

  def fetch_assignment(subject_identifier)
    @subject_storage.retrieve_assignment(self, subject_identifier)
  end

  protected

  def default_options
    {}
  end

  def should_store_assignment?(assignment)
    !assignment.returning? && (store_unqualified? || assignment.qualified?)
  end

  def assignment_with_unqualified_persistence(subject_identifier, subject, context)
    fetch_assignment(subject_identifier) || (
      subject_qualifies?(subject, context) ? 
        subject_assignment(subject_identifier, @segmenter.assign(subject_identifier, subject, context), false) :
        subject_assignment(subject_identifier, nil, false)
    )
  end

  def assignment_without_unqualified_persistence(subject_identifier, subject, context)
    if subject_qualifies?(subject, context)
      fetch_assignment(subject_identifier) ||
        subject_assignment(subject_identifier, @segmenter.assign(subject_identifier, subject, context), false)
    else 
      subject_assignment(subject_identifier, nil, false)
    end
  end  

  def subject_identifier(subject)
    subject.respond_to?(:id) ? subject.id : subject.to_s
  end

  def subject_qualifies?(subject, context = nil)
    everybody_qualifies? || @qualifier.call(subject, context)
  end
end

{'fraction_non_alphanumeric': 0.0874805598755832, 'fraction_numerical': 0.0, 'mean_word_length': 4.3925576519916145, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, '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': '750230', 'n_tokens_mistral': 3064, 'n_tokens_neox': 2970, 'n_words': 627}