lib/legato/query.rb
module Legato
class Query
include Enumerable
MONTH = 2592000
REQUEST_FIELDS = 'columnHeaders/name,rows,totalResults,totalsForAllResults,containsSampledData'
BASIC_OPTION_KEYS = [
:sort, :limit, :offset, :start_date, :end_date, :quota_user,
:user_ip, :sampling_level, :segment_id, :tracking_scope
]
VALID_TRACKING_SCOPES = {
'ga' => 'ga',
'mcf' => 'mcf',
'rt' => 'realtime'
}
def define_filter(name, &block)
(class << self; self; end).instance_eval do
define_method(name) {|*args| apply_filter(*args, &block)}
end
end
def define_segment_filter(name, &block)
(class << self; self; end).instance_eval do
define_method(name) {|*args| apply_segment_filter(*args, &block)}
end
end
def self.define_filter_operators(*methods)
methods.each do |method|
class_eval <<-CODE
def #{method}(field, value, join_character=nil)
Filter.new(self, field, :#{method}, value, join_character)
end
CODE
end
end
attr_reader :parent_klass
attr_accessor :profile, :start_date, :end_date
attr_accessor :sort, :limit, :offset, :quota_user, :user_ip, :sampling_level, :segment_id #, :segment # individual, overwritten
attr_accessor :filters, :segment_filters # combined, can be appended to
attr_accessor :tracking_scope
def self.from_query(query)
new(query.parent_klass, query.tracking_scope, query.filters, query.segment_filters)
end
def initialize(klass, tracking_scope = "ga", filters = FilterSet.new, segment_filters = FilterSet.new)
@loaded = false
@parent_klass = klass
self.filters = filters
self.segment_filters = segment_filters
self.start_date = Time.now - MONTH
self.end_date = Time.now
self.tracking_scope = tracking_scope
klass.filters.each do |name, block|
define_filter(name, &block)
end
klass.segments.each do |name, block|
define_segment_filter(name, &block)
end
end
def instance_klass
@parent_klass.instance_klass
end
def apply_filter(*args, &block)
apply_filter_expression(self.filters, *args, &block)
end
def apply_segment_filter(*args, &block)
apply_filter_expression(self.segment_filters, *args, &block)
end
def apply_filter_expression(filter_set, *args, &block)
# if given :filters or :segment_filters, make a set
filter_set = send(filter_set) if filter_set.is_a?(Symbol)
@profile = extract_profile(args)
join_character = Legato.and_join_character # filters are joined by AND
# # block returns one filter or an array of filters
Array.wrap(instance_exec(*args, &block)).each do |filter|
filter.join_character ||= join_character # only set when not set explicitly
filter_set << filter
join_character = Legato.or_join_character # arrays are joined by OR
end
self
end
def apply_options(options)
if options.has_key?(:sort)
# warn
options[:sort] = options.delete(:sort)
end
if options.has_key?(:profile)
self.profile = options.delete(:profile)
end
apply_basic_options(options)
# apply_filter_options(options[:filters])
self
end
def apply_basic_options(options)
BASIC_OPTION_KEYS.each do |key| #:segment
self.send("#{key}=".to_sym, options[key]) if options.has_key?(key)
end
end
# return a hash of basic options to merge
def basic_options
Hash[BASIC_OPTION_KEYS.map { |k| [k, send(k)] }].reject {|_,v| v.nil?}
end
# def apply_filter_options(filter_options)
# join_character = Legato.and_join_character
#
# Array.wrap(filter_options).compact.each do |filter|
# filter.each do |key, value|
# self.filters << hash_to_filter(key, value, join_character)
# join_character = Legato.and_join_character # hashes are joined by AND
# end
# join_character = Legato.or_join_character # arrays are joined by OR
# end
# end
# def hash_to_filter(key, value, join_character)
# field, operator = key, :eql
# field, operator = key.target, key.operator if key.is_a?(SymbolOperatorMethods)
# Filter.new(field, operator, value, join_character)
# end
def extract_profile(args)
return args.shift if args.first.is_a?(Management::Profile)
return args.pop if args.last.is_a?(Management::Profile)
profile
end
define_filter_operators :eql, :not_eql, :gt, :gte, :lt, :lte, :matches,
:does_not_match, :contains, :does_not_contain, :substring, :not_substring
def loaded?
@loaded
end
def load
response = request_for_query
@collection = response.collection
@total_results = response.total_results
@totals_for_all_results = response.totals_for_all_results
@sampled = response.sampled
@loaded = true
end
def collection
load unless loaded?
@collection
end
alias :to_a :collection
def total_results
load unless loaded?
@total_results
end
def totals_for_all_results
load unless loaded?
@totals_for_all_results
end
def sampled
load unless loaded?
@sampled
end
def each(&block)
collection.each(&block)
end
# if no filters, we use results to add profile
def results(profile=nil, options={})
query = loaded? ? Query.from_query(self) : self
options, profile = profile, self.profile if profile.is_a?(Hash)
query.profile = profile
query.apply_options(self.basic_options.merge(options))
query
end
# def total_results
# collection.total_results
# end
# def sampled?
# collection.sampled?
# end
def metrics
@metrics ||= parent_klass.metrics.dup
end
def dimensions
@dimensions ||= parent_klass.dimensions.dup
end
def sort=(arr)
@sort = Legato::ListParameter.new(:sort, arr)
end
def segment
"sessions::condition::#{segment_filters.to_params}" if segment_filters.any?
end
def segment_id=(segment_id)
@segment_id = "gaid::#{segment_id}"
end
def profile_id
profile && Legato.to_ga_string(profile.id)
end
def realtime?
tracking_scope == 'rt'
end
def realtime
self.tracking_scope = 'rt'
self
end
def to_params
base_params.tap do |params|
[metrics, dimensions, sort].compact.each do |list|
params.merge!(list.to_params(tracking_scope))
end
end
end
def to_query_string
to_params.map {|k,v| [k,v].join("=")}.join("&")
end
def base_url
raise "invalid tracking_scope" unless tracking_scope_valid?
endpoint = VALID_TRACKING_SCOPES[tracking_scope]
"https://www.googleapis.com/analytics/v3/data/#{endpoint}"
end
private
def base_params
{
'ids' => profile_id,
'start-date' => Legato.format_time(start_date),
'end-date' => Legato.format_time(end_date),
'max-results' => limit,
'start-index' => offset,
'segment' => segment_id || segment,
'filters' => filters.to_params, # defaults to AND filtering
'fields' => REQUEST_FIELDS,
'quotaUser' => quota_user,
'userIp' => user_ip,
'samplingLevel' => sampling_level
}.reject! {|_,v| v.nil? || v.to_s.strip.length == 0}
end
def tracking_scope_valid?
VALID_TRACKING_SCOPES.keys.include?(tracking_scope)
end
def request_for_query
profile.user.request(self)
end
end
end