Skip to content
This repository has been archived by the owner on Feb 20, 2021. It is now read-only.

[proposal] Add class definition style #60

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/verdict.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ def initialize(handle)
require "verdict/railtie" if defined?(Rails::Railtie)

require "verdict/metadata"
require "verdict/experiment_definition"
require "verdict/experiment"
require "verdict/group"
require "verdict/assignment"
Expand Down
247 changes: 247 additions & 0 deletions lib/verdict/experiment_definition.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
module Verdict
class ExperimentDefinition
extend Verdict::Metadata

class << self
attr_reader :handle

def inherited(subclass)
@handle = subclass.to_s.underscore.gsub('/', '_')
end

def event_logger
Verdict::EventLogger.new(Verdict.default_logger)
end

def subject_type
nil
end

def store_unqualified?
!!@store_unqualified
end

def manual_assignment_timestamps?
nil
end

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

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

def rollout_percentage(percentage, rollout_group_name = :enabled)
groups(Verdict::Segmenters::RolloutSegmenter) do
group(rollout_group_name, percentage)
end
end

def qualifiers
@qualifiers ||= []
end

def qualify(method_name = nil, &block)
if block_given?
qualifiers << block
elsif method_name.nil?
raise ArgumentError, "no method nor blocked passed!"
elsif respond_to?(method_name, true)
qualifiers << method(method_name).to_proc
else
raise ArgumentError, "No helper for #{method_name.inspect}"
end
end

def storage(storage = :memory, options = {})
@store_unqualified = options[:store_unqualified] if options.has_key?(:store_unqualified)
case storage
when :memory
Verdict::Storage::MemoryStorage.new
when :none
Verdict::Storage::MockStorage.new
when Class
storage.new
end
end

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

def started_at
@started_at ||= storage.retrieve_start_timestamp(self)
rescue Verdict::StorageError
nil
end

def started?
!@started_at.nil?
end

def group_handles
segmenter.groups.keys
end

def subject_assignment(subject, group, originally_created_at = nil, temporary = false)
Verdict::Assignment.new(self, subject, group, originally_created_at, temporary)
end

def subject_conversion(subject, goal, created_at = Time.now.utc)
Verdict::Conversion.new(self, subject, goal, created_at)
end

def convert(subject, goal)
identifier = retrieve_subject_identifier(subject)
conversion = subject_conversion(subject, goal)
event_logger.log_conversion(conversion)
segmenter.conversion_feedback(identifier, subject, conversion)
conversion
rescue Verdict::EmptySubjectIdentifier
raise unless disqualify_empty_identifier?
end

def assign(subject, context = nil)
previous_assignment = lookup(subject)

subject_identifier = retrieve_subject_identifier(subject)
assignment = if previous_assignment
previous_assignment
elsif subject_qualifies?(subject, context)
group = segmenter.assign(subject_identifier, subject, context)
subject_assignment(subject, group, nil, group.nil?)
else
nil_assignment(subject)
end

store_assignment(assignment)
rescue Verdict::StorageError
nil_assignment(subject)
rescue Verdict::EmptySubjectIdentifier
if disqualify_empty_identifier?
nil_assignment(subject)
else
raise
end
end

def assign_manually(subject, group)
assignment = subject_assignment(subject, group)
if !assignment.qualified? && !store_unqualified?
raise Verdict::Error, "Unqualified subject assignments are not stored for this experiment, so manual disqualification is impossible. Consider setting :store_unqualified to true for this experiment."
end

store_assignment(assignment)
assignment
end

def disqualify_manually(subject)
assign_manually(subject, nil)
end

def store_assignment(assignment)
storage.store_assignment(assignment) if should_store_assignment?(assignment)
event_logger.log_assignment(assignment)
assignment
end

def cleanup(options = {})
storage.cleanup(self, options)
end

def remove_subject_assignment(subject)
storage.remove_assignment(self, subject)
end

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

def lookup(subject)
storage.retrieve_assignment(self, subject)
end

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

def has_qualifier?
@qualifiers.any?
end

def everybody_qualifies?
!has_qualifier?
end

def as_json(options = {})
{
handle: handle,
has_qualifier: has_qualifier?,
groups: segmenter.groups.values.map { |group| group.as_json(options) },
metadata: metadata,
started_at: started_at.nil? ? nil : started_at.utc.strftime('%FT%TZ')
}.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 on identifier is not implemented for experiment #{handle.inspect}."
end

def disqualify_empty_identifier?
@disqualify_empty_identifier
end

def subject_qualifies?(subject, context = nil)
ensure_experiment_has_started
everybody_qualifies? || qualifiers.all? { |qualifier| qualifier.call(subject, context) }
end

protected

def default_options
{}
end

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

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

def set_start_timestamp
storage.store_start_timestamp(self, started_now = Time.now.utc)
started_now
rescue NotImplementedError
nil
end

def ensure_experiment_has_started
@started_at ||= started_at || set_start_timestamp
rescue Verdict::StorageError
@started_at ||= Time.now.utc
end

def nil_assignment(subject)
subject_assignment(subject, nil, nil)
end
end
end
end

7 changes: 0 additions & 7 deletions lib/verdict/railtie.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
class Verdict::Railtie < Rails::Railtie
initializer "experiments.configure_rails_initialization" do |app|
app.config.eager_load_namespaces << Verdict

Verdict.default_logger = Rails.logger

Verdict.directory ||= Rails.root.join('app', 'experiments')
app.config.eager_load_paths -= Dir[Verdict.directory.to_s]

# Re-freeze eager load paths to ensure they blow up if modified at runtime, as Rails does
app.config.eager_load_paths.freeze
end

config.to_prepare do
Expand Down
4 changes: 3 additions & 1 deletion lib/verdict/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
module Verdict
VERSION = "0.12.0"
class Version
VERSION = "0.13.0"
end
end
2 changes: 1 addition & 1 deletion verdict.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ require 'verdict/version'

Gem::Specification.new do |gem|
gem.name = "verdict"
gem.version = Verdict::VERSION
gem.version = Verdict::Version::VERSION
gem.authors = ["Shopify"]
gem.email = ["[email protected]", "[email protected]"]
gem.description = %q{Shopify Experiments classes}
Expand Down