origin-2.2.0/0000755000004100000410000000000012665643034013043 5ustar www-datawww-dataorigin-2.2.0/Rakefile0000644000004100000410000000125012665643034014506 0ustar www-datawww-datarequire "bundler" Bundler.setup require "rake" require "rspec" require "rspec/core/rake_task" $LOAD_PATH.unshift File.expand_path("../lib", __FILE__) require "origin/version" task :gem => :build task :build do system "gem build origin.gemspec" end task :install => :build do system "sudo gem install origin-#{Origin::VERSION}.gem" end task :release => :build do system "git tag -a v#{Origin::VERSION} -m 'Tagging #{Origin::VERSION}'" system "git push --tags" system "gem push origin-#{Origin::VERSION}.gem" system "rm origin-#{Origin::VERSION}.gem" end RSpec::Core::RakeTask.new("spec") do |spec| spec.pattern = "spec/**/*_spec.rb" end task :default => :spec origin-2.2.0/lib/0000755000004100000410000000000012665643034013611 5ustar www-datawww-dataorigin-2.2.0/lib/origin/0000755000004100000410000000000012665643034015100 5ustar www-datawww-dataorigin-2.2.0/lib/origin/selector.rb0000644000004100000410000001234112665643034017246 0ustar www-datawww-data# encoding: utf-8 module Origin # The selector is a special kind of hash that knows how to serialize values # coming into it as well as being alias and locale aware for key names. class Selector < Smash # Merges another selector into this one. # # @example Merge in another selector. # selector.merge!(name: "test") # # @param [ Hash, Selector ] other The object to merge in. # # @return [ Selector ] The selector. # # @since 1.0.0 def merge!(other) other.each_pair do |key, value| if value.is_a?(Hash) && self[key.to_s].is_a?(Hash) value = self[key.to_s].merge(value) do |_key, old_val, new_val| multi_value?(_key) ? (old_val + new_val).uniq : new_val end end if multi_selection?(key) value = (self[key.to_s] || []).concat(value) end store(key, value) end end # Store the value in the selector for the provided key. The selector will # handle all necessary serialization and localization in this step. # # @example Store a value in the selector. # selector.store(:key, "testing") # # @param [ String, Symbol ] key The name of the attribute. # @param [ Object ] value The value to add. # # @return [ Object ] The stored object. # # @since 1.0.0 def store(key, value) name, serializer = storage_pair(key) if multi_selection?(name) super(name, evolve_multi(value)) else super(normalized_key(name, serializer), evolve(serializer, value)) end end alias :[]= :store # Convert the selector to an aggregation pipeline entry. # # @example Convert the selector to a pipeline. # selector.to_pipeline # # @return [ Array ] The pipeline entry for the selector. # # @since 2.0.0 def to_pipeline pipeline = [] pipeline.push({ "$match" => self }) unless empty? pipeline end private # Evolves a multi-list selection, like an $and or $or criterion, and # performs the necessary serialization. # # @api private # # @example Evolve the multi-selection. # selector.evolve_multi([{ field: "value" }]) # # @param [ Array ] The multi-selection. # # @return [ Array ] The serialized values. # # @since 1.0.0 def evolve_multi(value) value.map do |val| Hash[val.map do |key, _value| _value = evolve_multi(_value) if multi_selection?(key) name, serializer = storage_pair(key) [ normalized_key(name, serializer), evolve(serializer, _value) ] end] end.uniq end # Evolve a single key selection with various types of values. # # @api private # # @example Evolve a simple selection. # selector.evolve(field, 5) # # @param [ Object ] serializer The optional serializer for the field. # @param [ Object ] value The value to serialize. # # @return [ Object ] The serialized object. # # @since 1.0.0 def evolve(serializer, value) case value when Hash evolve_hash(serializer, value) when Array evolve_array(serializer, value) else (serializer || value.class).evolve(value) end end # Evolve a single key selection with array values. # # @api private # # @example Evolve a simple selection. # selector.evolve(field, [ 1, 2, 3 ]) # # @param [ Object ] serializer The optional serializer for the field. # @param [ Array ] value The array to serialize. # # @return [ Object ] The serialized array. # # @since 1.0.0 def evolve_array(serializer, value) value.map do |_value| evolve(serializer, _value) end end # Evolve a single key selection with hash values. # # @api private # # @example Evolve a simple selection. # selector.evolve(field, { "$gt" => 5 }) # # @param [ Object ] serializer The optional serializer for the field. # @param [ Hash ] value The hash to serialize. # # @return [ Object ] The serialized hash. # # @since 1.0.0 def evolve_hash(serializer, value) value.each_pair do |operator, _value| if operator =~ /exists|type|size/ value[operator] = _value else value[operator] = evolve(serializer, _value) end end end # Determines if the selection is a multi-select, like an $and or $or or $nor # selection. # # @api private # # @example Is the selection a multi-select? # selector.multi_selection?("$and") # # @param [ String ] key The key to check. # # @return [ true, false ] If the key is for a multi-select. # # @since 1.0.0 def multi_selection?(key) key =~ /\$and|\$or|\$nor/ end # Determines if the selection operator takes a list. Returns true for $in and $nin. # # @api private # # @example Does the selection operator take multiple values? # selector.multi_value?("$nin") # # @param [ String ] key The key to check. # # @return [ true, false ] If the key is $in or $nin. # # @since 2.1.1 def multi_value?(key) key =~ /\$nin|\$in/ end end end origin-2.2.0/lib/origin/pipeline.rb0000644000004100000410000000477212665643034017244 0ustar www-datawww-data# encoding: utf-8 module Origin # Represents an aggregation pipeline. # # @since 2.0.0 class Pipeline < Array # @attribute [r] aliases The field aliases. attr_reader :aliases # Deep copy the aggregation pipeline. Will clone all the values in the # pipeline as well as the pipeline itself. # # @example Deep copy the pipeline. # pipeline.__deep_copy__ # # @return [ Pipeline ] The cloned pipeline. # # @since 2.0.0 def __deep_copy__ self.class.new(aliases) do |copy| each do |entry| copy.push(entry.__deep_copy__) end end end # Add a group operation to the aggregation pipeline. # # @example Add a group operation. # pipeline.group(:count.sum => 1, :max.max => "likes") # # @param [ Hash ] entry The group entry. # # @return [ Pipeline ] The pipeline. # # @since 2.0.0 def group(entry) push("$group" => evolve(entry.__expand_complex__)) end # Initialize the new pipeline. # # @example Initialize the new pipeline. # Origin::Pipeline.new(aliases) # # @param [ Hash ] aliases A hash of mappings from aliases to the actual # field names in the database. # # @since 2.0.0 def initialize(aliases = {}) @aliases = aliases yield(self) if block_given? end # Adds a $project entry to the aggregation pipeline. # # @example Add the projection. # pipeline.project(name: 1) # # @param [ Hash ] entry The projection. # # @return [ Pipeline ] The pipeline. def project(entry) push("$project" => evolve(entry)) end # Add the $unwind entry to the pipeline. # # @example Add the unwind. # pipeline.unwind(:field) # # @param [ String, Symbol ] field The name of the field. # # @return [ Pipeline ] The pipeline. # # @since 2.0.0 def unwind(field) normalized = field.to_s name = aliases[normalized] || normalized push("$unwind" => name.__mongo_expression__) end private # Evolve the entry using the aliases. # # @api private # # @example Evolve the entry. # pipeline.evolve(name: 1) # # @param [ Hash ] entry The entry to evolve. # # @return [ Hash ] The evolved entry. # # @since 2.0.0 def evolve(entry) aggregate = Selector.new(aliases) entry.each_pair do |field, value| aggregate.merge!(field.to_s => value) end aggregate end end end origin-2.2.0/lib/origin/aggregable.rb0000644000004100000410000000610312665643034017505 0ustar www-datawww-data# encoding: utf-8 module Origin # Provides a DSL around crafting aggregation framework commands. # # @since 2.0.0 module Aggregable extend Macroable # @attribute [r] pipeline The aggregation pipeline. attr_reader :pipeline # @attribute [rw] aggregating Flag for whether or not we are aggregating. attr_writer :aggregating # Has the aggregable enter an aggregation state. Ie, are only aggregation # operations allowed at this point on. # # @example Is the aggregable aggregating? # aggregable.aggregating? # # @return [ true, false ] If the aggregable is aggregating. # # @since 2.0.0 def aggregating? !!@aggregating end # Add a group ($group) operation to the aggregation pipeline. # # @example Add a group operation being verbose. # aggregable.group(count: { "$sum" => 1 }, max: { "$max" => "likes" }) # # @example Add a group operation using symbol shortcuts. # aggregable.group(:count.sum => 1, :max.max => "likes") # # @param [ Hash ] operation The group operation. # # @return [ Aggregable ] The aggregable. # # @since 2.0.0 def group(operation) aggregation(operation) do |pipeline| pipeline.group(operation) end end key :avg, :override, "$avg" key :max, :override, "$max" key :min, :override, "$min" key :sum, :override, "$sum" key :last, :override, "$last" key :push, :override, "$push" key :first, :override, "$first" key :add_to_set, :override, "$addToSet" # Add a projection ($project) to the aggregation pipeline. # # @example Add a projection to the pipeline. # aggregable.project(author: 1, name: 0) # # @param [ Hash ] criterion The projection to make. # # @return [ Aggregable ] The aggregable. # # @since 2.0.0 def project(operation = nil) aggregation(operation) do |pipeline| pipeline.project(operation) end end # Add an unwind ($unwind) to the aggregation pipeline. # # @example Add an unwind to the pipeline. # aggregable.unwind(:field) # # @param [ String, Symbol ] field The name of the field to unwind. # # @return [ Aggregable ] The aggregable. # # @since 2.0.0 def unwind(field) aggregation(field) do |pipeline| pipeline.unwind(field) end end private # Add the aggregation operation. # # @api private # # @example Aggregate on the operation. # aggregation(operation) do |pipeline| # pipeline.push("$project" => operation) # end # # @param [ Hash ] operation The operation for the pipeline. # # @return [ Aggregable ] The cloned aggregable. # # @since 2.0.0 def aggregation(operation) return self unless operation clone.tap do |query| unless aggregating? query.pipeline.concat(query.selector.to_pipeline) query.pipeline.concat(query.options.to_pipeline) query.aggregating = true end yield(query.pipeline) end end end end origin-2.2.0/lib/origin/optional.rb0000644000004100000410000002525312665643034017261 0ustar www-datawww-data# encoding: utf-8 module Origin # The optional module includes all behaviour that has to do with extra # options surrounding queries, like skip, limit, sorting, etc. module Optional extend Macroable # @attribute [rw] options The query options. attr_accessor :options # Add ascending sorting options for all the provided fields. # # @example Add ascending sorting. # optional.ascending(:first_name, :last_name) # # @param [ Array ] fields The fields to sort. # # @return [ Optional ] The cloned optional. # # @since 1.0.0 def ascending(*fields) sort_with_list(*fields, 1) end alias :asc :ascending key :asc, :override, 1 key :ascending, :override, 1 # Adds the option for telling MongoDB how many documents to retrieve in # it's batching. # # @example Apply the batch size options. # optional.batch_size(500) # # @param [ Integer ] value The batch size. # # @return [ Optional ] The cloned optional. # # @since 1.0.0 def batch_size(value = nil) option(value) { |options| options.store(:batch_size, value) } end # Add descending sorting options for all the provided fields. # # @example Add descending sorting. # optional.descending(:first_name, :last_name) # # @param [ Array ] fields The fields to sort. # # @return [ Optional ] The cloned optional. # # @since 1.0.0 def descending(*fields) sort_with_list(*fields, -1) end alias :desc :descending key :desc, :override, -1 key :descending, :override, -1 # Add an index hint to the query options. # # @example Add an index hint. # optional.hint("$natural" => 1) # # @param [ Hash ] value The index hint. # # @return [ Optional ] The cloned optional. # # @since 1.0.0 def hint(value = nil) option(value) { |options| options.store(:hint, value) } end # Add the number of documents to limit in the returned results. # # @example Limit the number of returned documents. # optional.limit(20) # # @param [ Integer ] value The number of documents to return. # # @return [ Optional ] The cloned optional. # # @since 1.0.0 def limit(value = nil) option(value) do |options, query| val = value.to_i options.store(:limit, val) query.pipeline.push("$limit" => val) if aggregating? end end # Adds the option to limit the number of documents scanned in the # collection. # # @example Add the max scan limit. # optional.max_scan(1000) # # @param [ Integer ] value The max number of documents to scan. # # @return [ Optional ] The cloned optional. # # @since 1.0.0 def max_scan(value = nil) option(value) { |options| options.store(:max_scan, value) } end # Tell the query not to timeout. # # @example Tell the query not to timeout. # optional.no_timeout # # @return [ Optional ] The cloned optional. # # @since 1.0.0 def no_timeout clone.tap { |query| query.options.store(:timeout, false) } end # Limits the results to only contain the fields provided. # # @example Limit the results to the provided fields. # optional.only(:name, :dob) # # @param [ Array ] args The fields to return. # # @return [ Optional ] The cloned optional. # # @since 1.0.0 def only(*args) args = args.flatten option(*args) do |options| options.store( :fields, args.inject(options[:fields] || {}){ |sub, field| sub.tap { sub[field] = 1 }} ) end end # Adds sorting criterion to the options. # # @example Add sorting options via a hash with integer directions. # optional.order_by(name: 1, dob: -1) # # @example Add sorting options via a hash with symbol directions. # optional.order_by(name: :asc, dob: :desc) # # @example Add sorting options via a hash with string directions. # optional.order_by(name: "asc", dob: "desc") # # @example Add sorting options via an array with integer directions. # optional.order_by([[ name, 1 ], [ dob, -1 ]]) # # @example Add sorting options via an array with symbol directions. # optional.order_by([[ name, :asc ], [ dob, :desc ]]) # # @example Add sorting options via an array with string directions. # optional.order_by([[ name, "asc" ], [ dob, "desc" ]]) # # @example Add sorting options with keys. # optional.order_by(:name.asc, :dob.desc) # # @example Add sorting options via a string. # optional.order_by("name ASC, dob DESC") # # @param [ Array, Hash, String ] spec The sorting specification. # # @return [ Optional ] The cloned optional. # # @since 1.0.0 def order_by(*spec) option(spec) do |options, query| spec.compact.each do |criterion| criterion.__sort_option__.each_pair do |field, direction| add_sort_option(options, field, direction) end query.pipeline.push("$sort" => options[:sort]) if aggregating? end end end alias :order :order_by # Instead of merging the order criteria, use this method to completely # replace the existing ordering with the provided. # # @example Replace the ordering. # optional.reorder(name: :asc) # # @param [ Array, Hash, String ] spec The sorting specification. # # @return [ Optional ] The cloned optional. # # @since 2.1.0 def reorder(*spec) options.delete(:sort) order_by(*spec) end # Add the number of documents to skip. # # @example Add the number to skip. # optional.skip(100) # # @param [ Integer ] value The number to skip. # # @return [ Optional ] The cloned optional. # # @since 1.0.0 def skip(value = nil) option(value) do |options, query| val = value.to_i options.store(:skip, val) query.pipeline.push("$skip" => val) if aggregating? end end alias :offset :skip # Limit the returned results via slicing embedded arrays. # # @example Slice the returned results. # optional.slice(aliases: [ 0, 5 ]) # # @param [ Hash ] criterion The slice options. # # @return [ Optional ] The cloned optional. # # @since 1.0.0 def slice(criterion = nil) option(criterion) do |options| options.__union__( fields: criterion.inject({}) do |option, (field, val)| option.tap { |opt| opt.store(field, { "$slice" => val }) } end ) end end # Tell the query to operate in snapshot mode. # # @example Add the snapshot option. # optional.snapshot # # @return [ Optional ] The cloned optional. # # @since 1.0.0 def snapshot clone.tap do |query| query.options.store(:snapshot, true) end end # Limits the results to only contain the fields not provided. # # @example Limit the results to the fields not provided. # optional.without(:name, :dob) # # @param [ Array ] args The fields to ignore. # # @return [ Optional ] The cloned optional. # # @since 1.0.0 def without(*args) args = args.flatten option(*args) do |options| options.store( :fields, args.inject(options[:fields] || {}){ |sub, field| sub.tap { sub[field] = 0 }} ) end end # Associate a comment with the query. # # @example Add a comment. # optional.comment('slow query') # # @note Set profilingLevel to 2 and the comment will be logged in the profile # collection along with the query. # # @param [ String ] comment The comment to be associated with the query. # # @return [ Optional ] The cloned optional. # # @since 2.2.0 def comment(comment = nil) clone.tap do |query| query.options.store(:comment, comment) end end # Set the cursor type. # # @example Set the cursor type. # optional.cursor_type(:tailable) # optional.cursor_type(:tailable_await) # # @note The cursor can be type :tailable or :tailable_await. # # @param [ Symbol ] type The type of cursor to create. # # @return [ Optional ] The cloned optional. # # @since 2.2.0 def cursor_type(type) clone.tap { |query| query.options.store(:cursor_type, type) } end private # Add a single sort option. # # @api private # # @example Add a single sort option. # optional.add_sort_option({}, :name, 1) # # @param [ Hash ] options The options. # @param [ String ] field The field name. # @param [ Integer ] direction The sort direction. # # @return [ Optional ] The cloned optional. # # @since 1.0.0 def add_sort_option(options, field, direction) if driver == :mongo1x sorting = (options[:sort] || []).dup sorting.push([ field, direction ]) options.store(:sort, sorting) else sorting = (options[:sort] || {}).dup sorting[field] = direction options.store(:sort, sorting) end end # Take the provided criterion and store it as an option in the query # options. # # @api private # # @example Store the option. # optional.option({ skip: 10 }) # # @param [ Array ] args The options. # # @return [ Queryable ] The cloned queryable. # # @since 1.0.0 def option(*args) clone.tap do |query| unless args.compact.empty? yield(query.options, query) end end end # Add multiple sort options at once. # # @api private # # @example Add multiple sort options. # optional.sort_with_list(:name, :dob, 1) # # @param [ Array ] fields The field names. # @param [ Integer ] direction The sort direction. # # @return [ Optional ] The cloned optional. # # @since 1.0.0 def sort_with_list(*fields, direction) option(fields) do |options, query| fields.flatten.compact.each do |field| add_sort_option(options, field, direction) end query.pipeline.push("$sort" => options[:sort]) if aggregating? end end class << self # Get the methods on the optional that can be forwarded to from a model. # # @example Get the forwardable methods. # Optional.forwardables # # @return [ Array ] The names of the forwardable methods. # # @since 1.0.0 def forwardables public_instance_methods(false) - [ :options, :options= ] end end end end origin-2.2.0/lib/origin/options.rb0000644000004100000410000000600712665643034017123 0ustar www-datawww-data# encoding: utf-8 module Origin # The options is a hash representation of options passed to MongoDB queries, # such as skip, limit, and sorting criteria. class Options < Smash # Convenience method for getting the field options. # # @example Get the fields options. # options.fields # # @return [ Hash ] The fields options. # # @since 1.0.0 def fields self[:fields] end # Convenience method for getting the limit option. # # @example Get the limit option. # options.limit # # @return [ Integer ] The limit option. # # @since 1.0.0 def limit self[:limit] end # Convenience method for getting the skip option. # # @example Get the skip option. # options.skip # # @return [ Integer ] The skip option. # # @since 1.0.0 def skip self[:skip] end # Convenience method for getting the sort options. # # @example Get the sort options. # options.sort # # @return [ Hash ] The sort options. # # @since 1.0.0 def sort self[:sort] end # Store the value in the options for the provided key. The options will # handle all necessary serialization and localization in this step. # # @example Store a value in the options. # options.store(:key, "testing") # # @param [ String, Symbol ] key The name of the attribute. # @param [ Object ] value The value to add. # # @return [ Object ] The stored object. # # @since 1.0.0 def store(key, value) super(key, evolve(value)) end alias :[]= :store # Convert the options to aggregation pipeline friendly options. # # @example Convert the options to a pipeline. # options.to_pipeline # # @return [ Array ] The options in pipeline form. # # @since 2.0.0 def to_pipeline pipeline = [] pipeline.push({ "$skip" => skip }) if skip pipeline.push({ "$limit" => limit }) if limit pipeline.push({ "$sort" => sort }) if sort pipeline end private # Evolve a single key selection with various types of values. # # @api private # # @example Evolve a simple selection. # options.evolve(field, 5) # # @param [ Object ] value The value to serialize. # # @return [ Object ] The serialized object. # # @since 1.0.0 def evolve(value) case value when Hash evolve_hash(value) else value end end # Evolve a single key selection with hash values. # # @api private # # @example Evolve a simple selection. # options.evolve(field, { "$gt" => 5 }) # # @param [ Hash ] value The hash to serialize. # # @return [ Object ] The serialized hash. # # @since 1.0.0 def evolve_hash(value) value.inject({}) do |hash, (field, _value)| name, serializer = storage_pair(field) hash[normalized_key(name, serializer)] = _value hash end end end end origin-2.2.0/lib/origin/queryable.rb0000644000004100000410000000476012665643034017425 0ustar www-datawww-data# encoding: utf-8 require "origin/extensions" require "origin/key" require "origin/macroable" require "origin/mergeable" require "origin/smash" require "origin/aggregable" require "origin/pipeline" require "origin/optional" require "origin/options" require "origin/selectable" require "origin/selector" module Origin # A queryable is any object that needs origin's dsl injected into it to build # MongoDB queries. For example, a Mongoid::Criteria is an Origin::Queryable. # # @example Include queryable functionality. # class Criteria # include Origin::Queryable # end module Queryable include Mergeable include Aggregable include Selectable include Optional # @attribute [r] aliases The aliases. # @attribute [r] driver The Mongo driver being used. # @attribute [r] serializers The serializers. attr_reader :aliases, :driver, :serializers # Is this queryable equal to another object? Is true if the selector and # options are equal. # # @example Are the objects equal? # queryable == criteria # # @param [ Object ] other The object to compare against. # # @return [ true, false ] If the objects are equal. # # @since 1.0.0 def ==(other) return false unless other.is_a?(Queryable) selector == other.selector && options == other.options end # Initialize the new queryable. Will yield itself to the block if a block # is provided for objects that need additional behaviour. # # @example Initialize the queryable. # Origin::Queryable.new # # @param [ Hash ] aliases The optional field aliases. # @param [ Hash ] serializers The optional field serializers. # @param [ Symbol ] driver The driver being used. # # @since 1.0.0 def initialize(aliases = {}, serializers = {}, driver = :mongo) @aliases, @driver, @serializers = aliases, driver.to_sym, serializers @options = Options.new(aliases, serializers) @selector = Selector.new(aliases, serializers) @pipeline = Pipeline.new(aliases) yield(self) if block_given? end # Handle the creation of a copy via #clone or #dup. # # @example Handle copy initialization. # queryable.initialize_copy(criteria) # # @param [ Queryable ] other The original copy. # # @since 1.0.0 def initialize_copy(other) @options = other.options.__deep_copy__ @selector = other.selector.__deep_copy__ @pipeline = other.pipeline.__deep_copy__ end end end origin-2.2.0/lib/origin/macroable.rb0000644000004100000410000000124412665643034017353 0ustar www-datawww-data# encoding: utf-8 module Origin # Adds macro behaviour for adding symbol methods. module Macroable # Adds a method on Symbol for convenience in where queries for the # provided operators. # # @example Add a symbol key. # key :all, "$all # # @param [ Symbol ] name The name of the method. # @param [ Symbol ] strategy The merge strategy. # @param [ String ] operator The MongoDB operator. # @param [ String ] additional The additional MongoDB operator. # # @since 1.0.0 def key(name, strategy, operator, additional = nil, &block) ::Symbol.add_key(name, strategy, operator, additional, &block) end end end origin-2.2.0/lib/origin/version.rb0000644000004100000410000000007012665643034017107 0ustar www-datawww-data# encoding: utf-8 module Origin VERSION = "2.2.0" end origin-2.2.0/lib/origin/smash.rb0000644000004100000410000000536612665643034016552 0ustar www-datawww-data# encoding: utf-8 module Origin # This is a smart hash for use with options and selectors. class Smash < Hash # @attribute [r] aliases The aliases. # @attribute [r] serializers The serializers. attr_reader :aliases, :serializers # Perform a deep copy of the smash. # # @example Perform a deep copy. # smash.__deep_copy__ # # @return [ Smash ] The copied hash. # # @since 1.0.0 def __deep_copy__ self.class.new(aliases, serializers) do |copy| each_pair do |key, value| copy.store(key, value.__deep_copy__) end end end # Initialize the new selector. # # @example Initialize the new selector. # Origin::Smash.new(aliases, serializers) # # @param [ Hash ] aliases A hash of mappings from aliases to the actual # field names in the database. # @param [ Hash ] serializers An optional hash of objects that are # responsible for serializing values. The keys of the hash must be # strings that match the field name, and the values must respond to # #localized? and #evolve(object). # # @since 1.0.0 def initialize(aliases = {}, serializers = {}) @aliases, @serializers = aliases, serializers yield(self) if block_given? end # Get an item from the smart hash by the provided key. # # @example Get an item by the key. # smash["test"] # # @param [ String ] key The key. # # @return [ Object ] The found object. # # @since 2.0.0 def [](key) fetch(aliases[key]) { super } end private # Get the normalized value for the key. If localization is in play the # current locale will be appended to the key in MongoDB dot notation. # # @api private # # @example Get the normalized key name. # smash.normalized_key("field", serializer) # # @param [ String ] name The name of the field. # @param [ Object ] serializer The optional field serializer. # # @return [ String ] The normalized key. # # @since 1.0.0 def normalized_key(name, serializer) serializer && serializer.localized? ? "#{name}.#{::I18n.locale}" : name end # Get the pair of objects needed to store the value in a hash by the # provided key. This is the database field name and the serializer. # # @api private # # @example Get the name and serializer. # smash.storage_pair("id") # # @param [ Symbol, String ] key The key provided to the selection. # # @return [ Array ] The name of the db field and # serializer. # # @since 1.0.0 def storage_pair(key) field = key.to_s name = aliases[field] || field [ name, serializers[name] ] end end end origin-2.2.0/lib/origin/selectable.rb0000644000004100000410000004457312665643034017545 0ustar www-datawww-data# encoding: utf-8 module Origin # An origin selectable is selectable, in that it has the ability to select # document from the database. The selectable module brings all functionality # to the selectable that has to do with building MongoDB selectors. module Selectable extend Macroable # Constant for a LineString $geometry. # # @since 2.0.0 LINE_STRING = "LineString" # Constant for a Point $geometry. # # @since 2.0.0 POINT = "Point" # Constant for a Polygon $geometry. # # @since 2.0.0 POLYGON = "Polygon" # @attribute [rw] negating If the next spression is negated. # @attribute [rw] selector The query selector. attr_accessor :negating, :selector # Add the $all criterion. # # @example Add the criterion. # selectable.all(field: [ 1, 2 ]) # # @example Execute an $all in a where query. # selectable.where(:field.all => [ 1, 2 ]) # # @param [ Hash ] criterion The key value pairs for $all matching. # # @return [ Selectable ] The cloned selectable. # # @since 1.0.0 def all(criterion = nil) send(strategy || :__union__, with_array_values(criterion), "$all") end alias :all_in :all key :all, :union, "$all" # Add the $and criterion. # # @example Add the criterion. # selectable.and({ field: value }, { other: value }) # # @param [ Array ] criterion Multiple key/value pair matches that # all must match to return results. # # @return [ Selectable ] The cloned selectable. # # @since 1.0.0 def and(*criterion) __multi__(criterion, "$and") end alias :all_of :and # Add the range selection. # # @example Match on results within a single range. # selectable.between(field: 1..2) # # @example Match on results between multiple ranges. # selectable.between(field: 1..2, other: 5..7) # # @param [ Hash ] criterion Multiple key/range pairs. # # @return [ Selectable ] The cloned selectable. # # @since 1.0.0 def between(criterion = nil) selection(criterion) do |selector, field, value| selector.store( field, { "$gte" => value.min, "$lte" => value.max } ) end end # Select with an $elemMatch. # # @example Add criterion for a single match. # selectable.elem_match(field: { name: "value" }) # # @example Add criterion for multiple matches. # selectable.elem_match( # field: { name: "value" }, # other: { name: "value"} # ) # # @example Execute an $elemMatch in a where query. # selectable.where(:field.elem_match => { name: "value" }) # # @param [ Hash ] criterion The field/match pairs. # # @return [ Selectable ] The cloned selectable. # # @since 1.0.0 def elem_match(criterion = nil) __override__(criterion, "$elemMatch") end key :elem_match, :override, "$elemMatch" # Add the $exists selection. # # @example Add a single selection. # selectable.exists(field: true) # # @example Add multiple selections. # selectable.exists(field: true, other: false) # # @example Execute an $exists in a where query. # selectable.where(:field.exists => true) # # @param [ Hash ] criterion The field/boolean existence checks. # # @return [ Selectable ] The cloned selectable. # # @since 1.0.0 def exists(criterion = nil) typed_override(criterion, "$exists") do |value| ::Boolean.evolve(value) end end key :exists, :override, "$exists" do |value| ::Boolean.evolve(value) end # Add a $geoIntersects or $geoWithin selection. Symbol operators must be used as shown in # the examples to expand the criteria. # # @note The only valid geometry shapes for a $geoIntersects are: # :intersects_line, :intersects_point, and :intersects_polygon. # # @note The only valid geometry shape for a $geoWithin is :within_polygon # # @example Add a geo intersect criterion for a line. # query.geo_spacial(:location.intersects_line => [[ 1, 10 ], [ 2, 10 ]]) # # @example Add a geo intersect criterion for a point. # query.geo_spacial(:location.intersects_point => [[ 1, 10 ]]) # # @example Add a geo intersect criterion for a polygon. # query.geo_spacial(:location.intersects_polygon => [[ 1, 10 ], [ 2, 10 ], [ 1, 10 ]]) # # @example Add a geo within criterion for a polygon. # query.geo_spacial(:location.within_polygon => [[ 1, 10 ], [ 2, 10 ], [ 1, 10 ]]) # # @param [ Hash ] criterion The criterion. # # @return [ Selectable ] The cloned selectable. # # @since 2.0.0 def geo_spacial(criterion = nil) __merge__(criterion) end key :intersects_line, :override, "$geoIntersects", "$geometry" do |value| { "type" => LINE_STRING, "coordinates" => value } end key :intersects_point, :override, "$geoIntersects", "$geometry" do |value| { "type" => POINT, "coordinates" => value } end key :intersects_polygon, :override, "$geoIntersects", "$geometry" do |value| { "type" => POLYGON, "coordinates" => value } end key :within_polygon, :override, "$geoWithin", "$geometry" do |value| { "type" => POLYGON, "coordinates" => value } end # Add the $gt criterion to the selector. # # @example Add the $gt criterion. # selectable.gt(age: 60) # # @example Execute an $gt in a where query. # selectable.where(:field.gt => 10) # # @param [ Hash ] criterion The field/value pairs to check. # # @return [ Selectable ] The cloned selectable. # # @since 1.0.0 def gt(criterion = nil) __override__(criterion, "$gt") end key :gt, :override, "$gt" # Add the $gte criterion to the selector. # # @example Add the $gte criterion. # selectable.gte(age: 60) # # @example Execute an $gte in a where query. # selectable.where(:field.gte => 10) # # @param [ Hash ] criterion The field/value pairs to check. # # @return [ Selectable ] The cloned selectable. # # @since 1.0.0 def gte(criterion = nil) __override__(criterion, "$gte") end key :gte, :override, "$gte" # Adds the $in selection to the selectable. # # @example Add $in selection on an array. # selectable.in(age: [ 1, 2, 3 ]) # # @example Add $in selection on a range. # selectable.in(age: 18..24) # # @example Execute an $in in a where query. # selectable.where(:field.in => [ 1, 2, 3 ]) # # @param [ Hash ] criterion The field/value criterion pairs. # # @return [ Selectable ] The cloned selectable. # # @since 1.0.0 def in(criterion = nil) send(strategy || :__intersect__, with_array_values(criterion), "$in") end alias :any_in :in key :in, :intersect, "$in" # Add the $lt criterion to the selector. # # @example Add the $lt criterion. # selectable.lt(age: 60) # # @example Execute an $lt in a where query. # selectable.where(:field.lt => 10) # # @param [ Hash ] criterion The field/value pairs to check. # # @return [ Selectable ] The cloned selectable. # # @since 1.0.0 def lt(criterion = nil) __override__(criterion, "$lt") end key :lt, :override, "$lt" # Add the $lte criterion to the selector. # # @example Add the $lte criterion. # selectable.lte(age: 60) # # @example Execute an $lte in a where query. # selectable.where(:field.lte => 10) # # @param [ Hash ] criterion The field/value pairs to check. # # @return [ Selectable ] The cloned selectable. # # @since 1.0.0 def lte(criterion = nil) __override__(criterion, "$lte") end key :lte, :override, "$lte" # Add a $maxDistance selection to the selectable. # # @example Add the $maxDistance selection. # selectable.max_distance(location: 10) # # @param [ Hash ] criterion The field/distance pairs. # # @return [ Selectable ] The cloned selectable. # # @since 1.0.0 def max_distance(criterion = nil) __add__(criterion, "$maxDistance") end # Adds $mod selection to the selectable. # # @example Add the $mod selection. # selectable.mod(field: [ 10, 1 ]) # # @example Execute an $mod in a where query. # selectable.where(:field.mod => [ 10, 1 ]) # # @param [ Hash ] criterion The field/mod selections. # # @return [ Selectable ] The cloned selectable. # # @since 1.0.0 def mod(criterion = nil) __override__(criterion, "$mod") end key :mod, :override, "$mod" # Adds $ne selection to the selectable. # # @example Query for a value $ne to something. # selectable.ne(field: 10) # # @example Execute an $ne in a where query. # selectable.where(:field.ne => "value") # # @param [ Hash ] criterion The field/ne selections. # # @return [ Selectable ] The cloned selectable. # # @since 1.0.0 def ne(criterion = nil) __override__(criterion, "$ne") end alias :excludes :ne key :ne, :override, "$ne" # Adds a $near criterion to a geo selection. # # @example Add the $near selection. # selectable.near(location: [ 23.1, 12.1 ]) # # @example Execute an $near in a where query. # selectable.where(:field.near => [ 23.2, 12.1 ]) # # @param [ Hash ] criterion The field/location pair. # # @return [ Selectable ] The cloned selectable. # # @since 1.0.0 def near(criterion = nil) __override__(criterion, "$near") end key :near, :override, "$near" # Adds a $nearSphere criterion to a geo selection. # # @example Add the $nearSphere selection. # selectable.near_sphere(location: [ 23.1, 12.1 ]) # # @example Execute an $nearSphere in a where query. # selectable.where(:field.near_sphere => [ 10.11, 3.22 ]) # # @param [ Hash ] criterion The field/location pair. # # @return [ Selectable ] The cloned selectable. # # @since 1.0.0 def near_sphere(criterion = nil) __override__(criterion, "$nearSphere") end key :near_sphere, :override, "$nearSphere" # Adds the $nin selection to the selectable. # # @example Add $nin selection on an array. # selectable.nin(age: [ 1, 2, 3 ]) # # @example Add $nin selection on a range. # selectable.nin(age: 18..24) # # @example Execute an $nin in a where query. # selectable.where(:field.nin => [ 1, 2, 3 ]) # # @param [ Hash ] criterion The field/value criterion pairs. # # @return [ Selectable ] The cloned selectable. # # @since 1.0.0 def nin(criterion = nil) send(strategy || :__intersect__, with_array_values(criterion), "$nin") end alias :not_in :nin key :nin, :intersect, "$nin" # Adds $nor selection to the selectable. # # @example Add the $nor selection. # selectable.nor(field: 1, field: 2) # # @param [ Array ] criterion An array of hash criterion. # # @return [ Selectable ] The cloned selectable. # # @since 1.0.0 def nor(*criterion) __multi__(criterion, "$nor") end # Is the current selectable negating the next selection? # # @example Is the selectable negating? # selectable.negating? # # @return [ true, false ] If the selectable is negating. # # @since 1.0.0 def negating? !!negating end # Negate the next selection. # # @example Negate the selection. # selectable.not.in(field: [ 1, 2 ]) # # @example Add the $not criterion. # selectable.not(name: /Bob/) # # @example Execute a $not in a where query. # selectable.where(:field.not => /Bob/) # # @param [ Hash ] criterion The field/value pairs to negate. # # @return [ Selectable ] The negated selectable. # # @since 1.0.0 def not(*criterion) if criterion.empty? tap { |query| query.negating = true } else __override__(criterion.first, "$not") end end key :not, :override, "$not" # Adds $or selection to the selectable. # # @example Add the $or selection. # selectable.or(field: 1, field: 2) # # @param [ Array ] criterion An array of hash criterion. # # @return [ Selectable ] The cloned selectable. # # @since 1.0.0 def or(*criterion) __multi__(criterion, "$or") end alias :any_of :or # Add a $size selection for array fields. # # @example Add the $size selection. # selectable.with_size(field: 5) # # @note This method is named #with_size not to conflict with any existing # #size method on enumerables or symbols. # # @example Execute an $size in a where query. # selectable.where(:field.with_size => 10) # # @param [ Hash ] criterion The field/size pairs criterion. # # @return [ Selectable ] The cloned selectable. # # @since 1.0.0 def with_size(criterion = nil) typed_override(criterion, "$size") do |value| ::Integer.evolve(value) end end key :with_size, :override, "$size" do |value| ::Integer.evolve(value) end # Adds a $type selection to the selectable. # # @example Add the $type selection. # selectable.with_type(field: 15) # # @example Execute an $type in a where query. # selectable.where(:field.with_type => 15) # # @note http://vurl.me/PGOU contains a list of all types. # # @param [ Hash ] criterion The field/type pairs. # # @return [ Selectable ] The cloned selectable. # # @since 1.0.0 def with_type(criterion = nil) typed_override(criterion, "$type") do |value| ::Integer.evolve(value) end end key :with_type, :override, "$type" do |value| ::Integer.evolve(value) end # Construct a text search selector. # # @example Construct a text search selector. # selectable.text_search("testing") # # @example Construct a text search selector with options. # selectable.text_search("testing", :$language => "fr") # # @param [ String, Symbol ] terms A string of terms that MongoDB parses # and uses to query the text index. # @param [ Hash ] opts Text search options. See MongoDB documentation # for options. # # @return [ Selectable ] The cloned selectable. # # @since 2.2.0 def text_search(terms, opts = nil) clone.tap do |query| if terms criterion = { :$text => { :$search => terms } } criterion[:$text].merge!(opts) if opts query.selector = criterion end end end # This is the general entry point for most MongoDB queries. This either # creates a standard field: value selection, and expanded selection with # the use of hash methods, or a $where selection if a string is provided. # # @example Add a standard selection. # selectable.where(name: "syd") # # @example Add a javascript selection. # selectable.where("this.name == 'syd'") # # @param [ String, Hash ] criterion The javascript or standard selection. # # @return [ Selectable ] The cloned selectable. # # @since 1.0.0 def where(criterion = nil) criterion.is_a?(String) ? js_query(criterion) : expr_query(criterion) end private # Create the standard expression query. # # @api private # # @example Create the selection. # selectable.expr_query(age: 50) # # @param [ Hash ] criterion The field/value pairs. # # @return [ Selectable ] The cloned selectable. # # @since 1.0.0 def expr_query(criterion) selection(criterion) do |selector, field, value| selector.merge!(field.__expr_part__(value.__expand_complex__, negating?)) end end # Force the values of the criterion to be evolved. # # @api private # # @example Force values to booleans. # selectable.force_typing(criterion) do |val| # Boolean.evolve(val) # end # # @param [ Hash ] criterion The criterion. # # @since 1.0.0 def typed_override(criterion, operator) if criterion criterion.update_values do |value| yield(value) end end __override__(criterion, operator) end # Create a javascript selection. # # @api private # # @example Create the javascript selection. # selectable.js_query("this.age == 50") # # @param [ String ] criterion The javascript as a string. # # @return [ Selectable ] The cloned selectable # # @since 1.0.0 def js_query(criterion) clone.tap do |query| query.selector.merge!("$where" => criterion) end end # Take the provided criterion and store it as a selection in the query # selector. # # @api private # # @example Store the selection. # selectable.selection({ field: "value" }) # # @param [ Hash ] criterion The selection to store. # # @return [ Selectable ] The cloned selectable. # # @since 1.0.0 def selection(criterion = nil) clone.tap do |query| if criterion criterion.each_pair do |field, value| yield(query.selector, field.is_a?(Key) ? field : field.to_s, value) end end query.reset_strategies! end end # Convert the criterion values to $in friendly values. This means you, # array. # # @api private # # @example Convert all the values to arrays. # selectable.with_array_values({ key: 1...4 }) # # @param [ Hash ] criterion The criterion. # # @return [ Hash ] The $in friendly criterion (array values). # # @since 1.0.0 def with_array_values(criterion) return nil unless criterion criterion.each_pair do |key, value| criterion[key] = value.__array__ end end class << self # Get the methods on the selectable that can be forwarded to from a model. # # @example Get the forwardable methods. # Selectable.forwardables # # @return [ Array ] The names of the forwardable methods. # # @since 1.0.0 def forwardables public_instance_methods(false) - [ :negating, :negating=, :negating?, :selector, :selector= ] end end end end origin-2.2.0/lib/origin/extensions.rb0000644000004100000410000000142112665643034017622 0ustar www-datawww-data# encoding: utf-8 unless defined?(Boolean) class Boolean; end end if defined?(ActiveSupport) unless defined?(ActiveSupport::TimeWithZone) require "active_support/time_with_zone" end require "origin/extensions/time_with_zone" end require "time" require "origin/extensions/object" require "origin/extensions/array" require "origin/extensions/big_decimal" require "origin/extensions/boolean" require "origin/extensions/date" require "origin/extensions/date_time" require "origin/extensions/hash" require "origin/extensions/nil_class" require "origin/extensions/numeric" require "origin/extensions/range" require "origin/extensions/regexp" require "origin/extensions/set" require "origin/extensions/string" require "origin/extensions/symbol" require "origin/extensions/time" origin-2.2.0/lib/origin/mergeable.rb0000644000004100000410000001606712665643034017362 0ustar www-datawww-data# encoding: utf-8 module Origin # Contains behaviour for merging existing selection with new selection. module Mergeable # @attribute [rw] strategy The name of the current strategy. attr_accessor :strategy # Instruct the next mergeable call to use intersection. # # @example Use intersection on the next call. # mergeable.intersect.in(field: [ 1, 2, 3 ]) # # @return [ Mergeable ] The intersect flagged mergeable. # # @since 1.0.0 def intersect use(:__intersect__) end # Instruct the next mergeable call to use override. # # @example Use override on the next call. # mergeable.override.in(field: [ 1, 2, 3 ]) # # @return [ Mergeable ] The override flagged mergeable. # # @since 1.0.0 def override use(:__override__) end # Instruct the next mergeable call to use union. # # @example Use union on the next call. # mergeable.union.in(field: [ 1, 2, 3 ]) # # @return [ Mergeable ] The union flagged mergeable. # # @since 1.0.0 def union use(:__union__) end # Reset the stratgies to nil, used after cloning. # # @example Reset the strategies. # mergeable.reset_strategies! # # @return [ nil ] nil. # # @since 1.0.0 def reset_strategies! self.strategy, self.negating = nil, nil end private # Adds the criterion to the existing selection. # # @api private # # @example Add the criterion. # mergeable.__add__({ name: 1 }, "$in") # # @param [ Hash ] criterion The criteria. # @param [ String ] operator The MongoDB operator. # # @return [ Mergeable ] The new mergeable. # # @since 1.0.0 def __add__(criterion, operator) with_strategy(:__add__, criterion, operator) end # Adds the criterion to the existing selection. # # @api private # # @example Add the criterion. # mergeable.__expanded__([ 1, 10 ], "$within", "$center") # # @param [ Hash ] criterion The criteria. # @param [ String ] outer The outer MongoDB operator. # @param [ String ] inner The inner MongoDB operator. # # @return [ Mergeable ] The new mergeable. # # @since 1.0.0 def __expanded__(criterion, outer, inner) selection(criterion) do |selector, field, value| selector.store(field, { outer => { inner => value }}) end end # Perform a straight merge of the criterion into the selection and let the # symbol overrides do all the work. # # @api private # # @example Straight merge the expanded criterion. # mergeable.__merge__(location: [ 1, 10 ]) # # @param [ Hash ] criterion The criteria. # # @return [ Mergeable ] The cloned object. # # @since 2.0.0 def __merge__(criterion) selection(criterion) do |selector, field, value| selector.merge!(field.__expr_part__(value)) end end # Adds the criterion to the existing selection. # # @api private # # @example Add the criterion. # mergeable.__intersect__([ 1, 2 ], "$in") # # @param [ Hash ] criterion The criteria. # @param [ String ] operator The MongoDB operator. # # @return [ Mergeable ] The new mergeable. # # @since 1.0.0 def __intersect__(criterion, operator) with_strategy(:__intersect__, criterion, operator) end # Adds the criterion to the existing selection. # # @api private # # @example Add the criterion. # mergeable.__multi__([ 1, 2 ], "$in") # # @param [ Hash ] criterion The criteria. # @param [ String ] operator The MongoDB operator. # # @return [ Mergeable ] The new mergeable. # # @since 1.0.0 def __multi__(criterion, operator) clone.tap do |query| sel = query.selector criterion.flatten.each do |expr| next unless expr criteria = sel[operator] || [] normalized = expr.inject({}) do |hash, (field, value)| hash.merge!(field.__expr_part__(value.__expand_complex__)) hash end sel.store(operator, criteria.push(normalized)) end end end # Adds the criterion to the existing selection. # # @api private # # @example Add the criterion. # mergeable.__override__([ 1, 2 ], "$in") # # @param [ Hash ] criterion The criteria. # @param [ String ] operator The MongoDB operator. # # @return [ Mergeable ] The new mergeable. # # @since 1.0.0 def __override__(criterion, operator) selection(criterion) do |selector, field, value| expression = prepare(field, operator, value) existing = selector[field] if existing.respond_to?(:merge!) selector.store(field, existing.merge!(expression)) else selector.store(field, expression) end end end # Adds the criterion to the existing selection. # # @api private # # @example Add the criterion. # mergeable.__union__([ 1, 2 ], "$in") # # @param [ Hash ] criterion The criteria. # @param [ String ] operator The MongoDB operator. # # @return [ Mergeable ] The new mergeable. # # @since 1.0.0 def __union__(criterion, operator) with_strategy(:__union__, criterion, operator) end # Use the named strategy for the next operation. # # @api private # # @example Use intersection. # mergeable.use(:__intersect__) # # @param [ Symbol ] strategy The strategy to use. # # @return [ Mergeable ] The existing mergeable. # # @since 1.0.0 def use(strategy) tap do |mergeable| mergeable.strategy = strategy end end # Add criterion to the selection with the named strategy. # # @api private # # @example Add criterion with a strategy. # mergeable.with_strategy(:__union__, [ 1, 2, 3 ], "$in") # # @param [ Symbol ] strategy The name of the strategy method. # @param [ Object ] criterion The criterion to add. # @param [ String ] operator The MongoDB operator. # # @return [ Mergeable ] The cloned query. # # @since 1.0.0 def with_strategy(strategy, criterion, operator) selection(criterion) do |selector, field, value| selector.store( field, selector[field].send(strategy, prepare(field, operator, value)) ) end end # Prepare the value for merging. # # @api private # # @example Prepare the value. # mergeable.prepare("field", "$gt", 10) # # @param [ String ] field The name of the field. # @param [ Object ] value The value. # # @return [ Object ] The serialized value. # # @since 1.0.0 def prepare(field, operator, value) unless operator =~ /exists|type|size/ value = value.__expand_complex__ serializer = serializers[field] value = serializer ? serializer.evolve(value) : value end selection = { operator => value } negating? ? { "$not" => selection } : selection end end end origin-2.2.0/lib/origin/key.rb0000644000004100000410000000551212665643034016220 0ustar www-datawww-data# encoding: utf-8 module Origin # The key is a representation of a field in a queryable, that can be # expanded to special MongoDB selectors. class Key # @attribute [r] name The name of the field. # @attribute [r] block The optional block to transform values. # @attribute [r] operator The MongoDB query operator. # @attribute [r] expanded The MongoDB expanded query operator. # @attribute [r] strategy The name of the merge strategy. attr_reader :block, :name, :operator, :expanded, :strategy # Does the key equal another object? # # @example Is the key equal to another? # key == other # key.eql? other # # @param [ Object ] other The object to compare to. # # @return [ true, false ] If the objects are equal. # # @since 1.0.0 def ==(other) return false unless other.is_a?(Key) name == other.name && operator == other.operator && expanded == other.expanded end alias :eql? :== # Calculate the hash code for a key. # # @return [ Fixnum ] The hash code for the key. # # @since 1.1.0 def hash [name, operator, expanded].hash end # Instantiate the new key. # # @example Instantiate the key. # Key.new("age", "$gt") # # @param [ String, Symbol ] name The field name. # @param [ Symbol ] strategy The name of the merge strategy. # @param [ String ] operator The Mongo operator. # @param [ String ] expanded The Mongo expanded operator. # # @since 1.0.0 def initialize(name, strategy, operator, expanded = nil, &block) @name, @strategy, @operator, @expanded, @block = name, strategy, operator, expanded, block end # Gets the raw selector that would be passed to Mongo from this key. # # @example Specify the raw selector. # key.__expr_part__(50) # # @param [ Object ] object The value to be included. # @param [ true, false ] negating If the selection should be negated. # # @return [ Hash ] The raw MongoDB selector. # # @since 1.0.0 def __expr_part__(object, negating = false) value = block ? block[object] : object expression = { operator => expanded ? { expanded => value } : value } { name.to_s => (negating && operator != "$not") ? { "$not" => expression } : expression } end # Get the key as raw Mongo sorting options. # # @example Get the key as a sort. # key.__sort_option__ # # @return [ Hash ] The field/direction pair. # # @since 1.0.0 def __sort_option__ { name => operator } end alias :__sort_pair__ :__sort_option__ # Convert the key to a string. # # @example Convert the key to a string. # key.to_s # # @return [ String ] The key as a string. # # @since 1.1.0 def to_s @name.to_s end end end origin-2.2.0/lib/origin/extensions/0000755000004100000410000000000012665643034017277 5ustar www-datawww-dataorigin-2.2.0/lib/origin/extensions/time_with_zone.rb0000644000004100000410000000202512665643034022647 0ustar www-datawww-data# encoding: utf-8 module Origin module Extensions # This module contains additional time with zone behaviour. module TimeWithZone # Evolve the time into a utc time. # # @example Evolve the time. # time.__evolve_time__ # # @return [ Time ] The time in UTC. # # @since 1.0.0 def __evolve_time__ utc end module ClassMethods # Evolve the object to an date. # # @example Evolve dates. # # @example Evolve string dates. # # @example Evolve date ranges. # # @param [ Object ] object The object to evolve. # # @return [ Time ] The evolved date time. # # @since 1.0.0 def evolve(object) object.__evolve_time__ end end end end end ::ActiveSupport::TimeWithZone.__send__( :include, Origin::Extensions::TimeWithZone ) ::ActiveSupport::TimeWithZone.__send__( :extend, Origin::Extensions::TimeWithZone::ClassMethods ) origin-2.2.0/lib/origin/extensions/string.rb0000644000004100000410000000711712665643034021140 0ustar www-datawww-data# encoding: utf-8 module Origin module Extensions # This module contains additional object behaviour. module String # Evolve the string into a mongodb friendly date. # # @example Evolve the string. # "2012-1-1".__evolve_date__ # # @return [ Time ] The time at UTC midnight. # # @since 1.0.0 def __evolve_date__ time = ::Time.parse(self) ::Time.utc(time.year, time.month, time.day, 0, 0, 0, 0) end # Evolve the string into a mongodb friendly time. # # @example Evolve the string. # "2012-1-1".__evolve_time__ # # @return [ Time ] The string as a time. # # @since 1.0.0 def __evolve_time__ ::Time.parse(self).utc end # Get the string as a mongo expression, adding $ to the front. # # @example Get the string as an expression. # "test".__mongo_expression__ # # @return [ String ] The string with $ at the front. # # @since 2.0.0 def __mongo_expression__ start_with?("$") ? self : "$#{self}" end # Get the string as a sort option. # # @example Get the string as a sort option. # "field ASC".__sort_option__ # # @return [ Hash ] The string as a sort option hash. # # @since 1.0.0 def __sort_option__ split(/,/).inject({}) do |hash, spec| hash.tap do |_hash| field, direction = spec.strip.split(/\s/) _hash[field.to_sym] = direction.to_direction end end end # Get the string as a specification. # # @example Get the string as a criteria. # "field".__expr_part__(value) # # @param [ Object ] value The value of the criteria. # @param [ true, false ] negating If the selection should be negated. # # @return [ Hash ] The selection. # # @since 1.0.0 def __expr_part__(value, negating = false) ::String.__expr_part__(self, value, negating) end # Get the string as a sort direction. # # @example Get the string as a sort direction. # "1".to_direction # # @return [ Integer ] The direction. # # @since 1.0.0 def to_direction self =~ /desc/i ? -1 : 1 end module ClassMethods # Get the value as a expression. # # @example Get the value as an expression. # String.__expr_part__("field", value) # # @param [ String, Symbol ] key The field key. # @param [ Object ] value The value of the criteria. # @param [ true, false ] negating If the selection should be negated. # # @return [ Hash ] The selection. # # @since 2.0.0 def __expr_part__(key, value, negating = false) if negating { key => { "$#{value.regexp? ? "not" : "ne"}" => value }} else { key => value } end end # Evolves the string into a MongoDB friendly value - in this case # a string. # # @example Evolve the string # String.evolve(1) # # @param [ Object ] object The object to convert. # # @return [ String ] The value as a string. # # @since 1.0.0 def evolve(object) __evolve__(object) do |obj| obj.regexp? ? obj : obj.to_s end end end end end end ::String.__send__(:include, Origin::Extensions::String) ::String.__send__(:extend, Origin::Extensions::String::ClassMethods) origin-2.2.0/lib/origin/extensions/hash.rb0000644000004100000410000001234612665643034020555 0ustar www-datawww-data# encoding: utf-8 module Origin module Extensions # This module contains additional hash behaviour. module Hash # Add an object to a hash using the merge strategies. # # @example Add an object to a hash. # { field: value }.__add__({ field: other_value }) # # @param [ Hash ] object The other hash to add. # # @return [ Hash ] The hash with object added. # # @since 1.0.0 def __add__(object) apply_strategy(:__add__, object) end # Merge this hash into the provided array. # # @example Merge the hash into the array. # { field: value }.__add_from_array__([ 1, 2 ]) # # @param [ Array ] value The array to add to. # # @return [ Hash ] The merged hash. # # @since 1.0.0 def __add_from_array__(array) { keys.first => array.__add__(values.first) } end # Add an object to a hash using the merge strategies. # # @example Add an object to a hash. # { field: value }.__intersect__({ field: other_value }) # # @param [ Hash ] object The other hash to intersect. # # @return [ Hash ] The hash with object intersected. # # @since 1.0.0 def __intersect__(object) apply_strategy(:__intersect__, object) end # Merge this hash into the provided array. # # @example Merge the hash into the array. # { field: value }.__intersect_from_array__([ 1, 2 ]) # # @param [ Array ] value The array to intersect to. # # @return [ Hash ] The merged hash. # # @since 1.0.0 def __intersect_from_array__(array) { keys.first => array.__intersect__(values.first) } end # Merge this hash into the provided object. # # @example Merge the hash into the object. # { field: value }.__intersect_from_object__([ 1, 2 ]) # # @param [ Object ] value The object to intersect to. # # @return [ Hash ] The merged hash. # # @since 1.0.0 def __intersect_from_object__(object) { keys.first => object.__intersect__(values.first) } end # Add an object to a hash using the merge strategies. # # @example Add an object to a hash. # { field: value }.__union__({ field: other_value }) # # @param [ Hash ] object The other hash to union. # # @return [ Hash ] The hash with object unioned. # # @since 1.0.0 def __union__(object) apply_strategy(:__union__, object) end # Merge this hash into the provided object. # # @example Merge the hash into the object. # { field: value }.__union_from_object__([ 1, 2 ]) # # @param [ Object ] value The object to union to. # # @return [ Hash ] The merged hash. # # @since 1.0.0 def __union_from_object__(object) { keys.first => object.__union__(values.first) } end # Make a deep copy of this hash. # # @example Make a deep copy of the hash. # { field: value }.__deep_copy__ # # @return [ Hash ] The copied hash. # # @since 1.0.0 def __deep_copy__ {}.tap do |copy| each_pair do |key, value| copy.store(key, value.__deep_copy__) end end end # Get the hash as a sort option. # # @example Get the hash as a sort option. # { field: 1 }.__sort_option__ # # @return [ Hash ] The hash as sort option. # # @since 1.0.0 def __sort_option__ tap do |hash| hash.each_pair do |key, value| hash.store(key, value.to_direction) end end end # Get the object as expanded. # # @example Get the object expanded. # obj.__expand_complex__ # # @return [ Hash ] The expanded hash. # # @since 1.0.5 def __expand_complex__ replacement = {} each_pair do |key, value| replacement.merge!(key.__expr_part__(value.__expand_complex__)) end replacement end # Update all the values in the hash with the provided block. # # @example Update the values in place. # { field: "1" }.update_values(&:to_i) # # @param [ Proc ] block The block to execute on each value. # # @return [ Hash ] the hash. # # @since 1.0.0 def update_values(&block) each_pair do |key, value| store(key, block[value]) end end private # Apply the provided strategy for the hash with the given object. # # @api private # # @example Apply the strategy. # { field: value }.apply_strategy(:__add__, 1) # # @param [ Symbol ] strategy The strategy to apply. # @param [ Object ] object The object to merge. # # @return [ Hash ] The merged hash. # # @since 1.0.0 def apply_strategy(strategy, object) tap do |hash| object.each_pair do |key, value| hash.store(key, hash[key].send(strategy, value)) end end end end end end ::Hash.__send__(:include, Origin::Extensions::Hash) origin-2.2.0/lib/origin/extensions/date_time.rb0000644000004100000410000000227212665643034021562 0ustar www-datawww-data# encoding: utf-8 module Origin module Extensions # This module contains additional datetime behaviour. module DateTime # Evolve the date time into a mongo friendly UTC time. # # @example Evolve the date time. # date_time.__evolve_time__ # # @return [ Time ] The converted time in UTC. # # @since 1.0.0 def __evolve_time__ usec = strftime("%6N").to_f if utc? ::Time.utc(year, month, day, hour, min, sec, usec) else ::Time.local(year, month, day, hour, min, sec, usec).utc end end module ClassMethods # Evolve the object to an date. # # @example Evolve dates. # # @example Evolve string dates. # # @example Evolve date ranges. # # @param [ Object ] object The object to evolve. # # @return [ Time ] The evolved date time. # # @since 1.0.0 def evolve(object) object.__evolve_time__ end end end end end ::DateTime.__send__(:include, Origin::Extensions::DateTime) ::DateTime.__send__(:extend, Origin::Extensions::DateTime::ClassMethods) origin-2.2.0/lib/origin/extensions/set.rb0000644000004100000410000000127212665643034020421 0ustar www-datawww-data# encoding: utf-8 require "set" module Origin module Extensions # This module contains additional object behaviour. module Set module ClassMethods # Evolve the set, casting all its elements. # # @example Evolve the set. # Set.evolve(set) # # @param [ Set, Object ] object The object to evolve. # # @return [ Array ] The evolved set. # # @since 1.0.0 def evolve(object) return object if !object || !object.respond_to?(:map) object.map{ |obj| obj.class.evolve(obj) } end end end end end ::Set.__send__(:extend, Origin::Extensions::Set::ClassMethods) origin-2.2.0/lib/origin/extensions/time.rb0000644000004100000410000000237012665643034020564 0ustar www-datawww-data# encoding: utf-8 module Origin module Extensions # This module contains additional time behaviour. module Time # Evolve the time as a date, UTC midnight. # # @example Evolve the time to a date query format. # time.__evolve_date__ # # @return [ Time ] The date at midnight UTC. # # @since 1.0.0 def __evolve_date__ ::Time.utc(year, month, day, 0, 0, 0, 0) end # Evolve the time into a utc time. # # @example Evolve the time. # time.__evolve_time__ # # @return [ Time ] The time in UTC. # # @since 1.0.0 def __evolve_time__ utc end module ClassMethods # Evolve the object to an date. # # @example Evolve dates. # # @example Evolve string dates. # # @example Evolve date ranges. # # @param [ Object ] object The object to evolve. # # @return [ Time ] The evolved date time. # # @since 1.0.0 def evolve(object) object.__evolve_time__ end end end end end ::Time.__send__(:include, Origin::Extensions::Time) ::Time.__send__(:extend, Origin::Extensions::Time::ClassMethods) origin-2.2.0/lib/origin/extensions/date.rb0000644000004100000410000000272412665643034020546 0ustar www-datawww-data# encoding: utf-8 module Origin module Extensions # This module contains additional date behaviour. module Date # Evolve the date into a mongo friendly time, UTC midnight. # # @example Evolve the date. # date.__evolve_date__ # # @return [ Time ] The date as a UTC time at midnight. # # @since 1.0.0 def __evolve_date__ ::Time.utc(year, month, day, 0, 0, 0, 0) end # Evolve the date into a time, which is always in the local timezone. # # @example Evolve the date. # date.__evolve_time__ # # @return [ Time ] The date as a local time. # # @since 1.0.0 def __evolve_time__ ::Time.local(year, month, day) end module ClassMethods # Evolve the object to an date. # # @example Evolve dates. # Date.evolve(Date.new(1990, 1, 1)) # # @example Evolve string dates. # Date.evolve("1990-1-1") # # @example Evolve date ranges. # Date.evolve(Date.new(1990, 1, 1)..Date.new(1990, 1, 4)) # # @param [ Object ] object The object to evolve. # # @return [ Time ] The evolved date. # # @since 1.0.0 def evolve(object) object.__evolve_date__ end end end end end ::Date.__send__(:include, Origin::Extensions::Date) ::Date.__send__(:extend, Origin::Extensions::Date::ClassMethods) origin-2.2.0/lib/origin/extensions/object.rb0000644000004100000410000001164612665643034021102 0ustar www-datawww-data# encoding: utf-8 module Origin module Extensions # This module contains additional object behaviour. module Object # Combine the two objects using the add strategy. # # @example Add the object to the array. # [ 1, 2, 3 ].__add__(4) # # @param [ Object ] object The object to add. # # @return [ Object ] The result of the add. # # @since 1.0.0 def __add__(object) (object == self) ? self : [ self, object ].flatten.uniq end # Merge this object into the provided array. # # @example Merge the object into the array. # 4.__add_from_array__([ 1, 2 ]) # # @param [ Array ] value The array to add to. # # @return [ Array ] The merged object. # # @since 1.0.0 def __add_from_array__(array) array.concat(Array(self)).uniq end # Combine the two objects using the intersect strategy. # # @example Add the object to the array. # [ 1, 2, 3 ].__intersect__(4) # # @param [ Object ] object The object to intersect. # # @return [ Array ] The result of the intersect. # # @since 1.0.0 def __intersect__(object) object.__intersect_from_object__(self) end # Merge this object into the provided array. # # @example Merge the object into the array. # 4.__intersect_from_array__([ 1, 2 ]) # # @param [ Array ] value The array to intersect to. # # @return [ Array ] The merged object. # # @since 1.0.0 def __intersect_from_array__(array) array & Array(self) end # Merge this object into the provided array. # # @example Merge the object into the array. # 4.__intersect_from_object__([ 1, 2 ]) # # @param [ Object ] value The value to intersect to. # # @return [ Array ] The merged object. # # @since 1.0.0 def __intersect_from_object__(object) Array(object) & Array(self) end # Combine the two objects using the union strategy. # # @example Add the object to the array. # [ 1, 2, 3 ].__union__(4) # # @param [ Object ] object The object to union. # # @return [ Array ] The result of the union. # # @since 1.0.0 def __union__(object) object.__union_from_object__(self) end # Merge this object into the provided array. # # @example Merge the object into the array. # 4.__union_from_object__([ 1, 2 ]) # # @param [ Object ] value The value to union to. # # @return [ Array ] The merged object. # # @since 1.0.0 def __union_from_object__(object) (Array(object) + Array(self)).uniq end # Deep copy the object. This is for API compatibility, but needs to be # overridden. # # @example Deep copy the object. # 1.__deep_copy__ # # @return [ Object ] self. # # @since 1.0.0 def __deep_copy__; self; end # Get the object as an array. # # @example Get the object as an array. # 4.__array__ # # @return [ Array ] The wrapped object. # # @since 1.0.0 def __array__ [ self ] end # Get the object as expanded. # # @example Get the object expanded. # obj.__expand_complex__ # # @return [ Object ] self. # # @since 1.0.5 def __expand_complex__ self end # Is the object a regex. # # @example Is the object a regex? # obj.regexp? # # @return [ false ] Always false. # # @since 1.0.4 def regexp? false end module ClassMethods # Evolve the object. # # @note This is here for API compatibility. # # @example Evolve an object. # Object.evolve("test") # # @return [ Object ] The provided object. # # @since 1.0.0 def evolve(object) object end private # Evolve the object. # # @api private # # @todo Durran refactor out case statement. # # @example Evolve an object and yield. # Object.evolve("test") do |obj| # obj.to_s # end # # @return [ Object ] The evolved object. # # @since 1.0.0 def __evolve__(object) return nil if object.nil? case object when ::Array object.map{ |obj| evolve(obj) } when ::Range { "$gte" => evolve(object.min), "$lte" => evolve(object.max) } else yield(object) end end end end end end ::Object.__send__(:include, Origin::Extensions::Object) ::Object.__send__(:extend, Origin::Extensions::Object::ClassMethods) origin-2.2.0/lib/origin/extensions/big_decimal.rb0000644000004100000410000000145412665643034022047 0ustar www-datawww-data# encoding: utf-8 require "bigdecimal" module Origin module Extensions # The big decimal module adds custom behaviour for Origin onto the # BigDecimal class. module BigDecimal module ClassMethods # Evolves the big decimal into a MongoDB friendly value - in this case # a string. # # @example Evolve the big decimal # BigDecimal.evolve(decimal) # # @param [ BigDecimal ] object The object to convert. # # @return [ String ] The big decimal as a string. # # @since 1.0.0 def evolve(object) __evolve__(object) do |obj| obj ? obj.to_s : obj end end end end end end ::BigDecimal.__send__(:extend, Origin::Extensions::BigDecimal::ClassMethods) origin-2.2.0/lib/origin/extensions/range.rb0000644000004100000410000000335412665643034020725 0ustar www-datawww-data# encoding: utf-8 module Origin module Extensions # This module contains additional range behaviour. module Range # Get the range as an array. # # @example Get the range as an array. # 1...3.__array__ # # @return [ Array ] The range as an array. # # @since 1.0.0 def __array__ to_a end # Convert the range to a min/max mongo friendly query for dates. # # @example Evolve the range. # (11231312..213123131).__evolve_date__ # # @return [ Hash ] The min/max range query with times at midnight. # # @since 1.0.0 def __evolve_date__ { "$gte" => min.__evolve_date__, "$lte" => max.__evolve_date__ } end # Convert the range to a min/max mongo friendly query for times. # # @example Evolve the range. # (11231312..213123131).__evolve_date__ # # @return [ Hash ] The min/max range query with times. # # @since 1.0.0 def __evolve_time__ { "$gte" => min.__evolve_time__, "$lte" => max.__evolve_time__ } end module ClassMethods # Evolve the range. This will transform it into a $gte/$lte selection. # # @example Evolve the range. # Range.evolve(1..3) # # @param [ Range ] object The range to evolve. # # @return [ Hash ] The range as a gte/lte criteria. # # @since 1.0.0 def evolve(object) return object unless object.is_a?(::Range) { "$gte" => object.min, "$lte" => object.max } end end end end end ::Range.__send__(:include, Origin::Extensions::Range) ::Range.__send__(:extend, Origin::Extensions::Range::ClassMethods) origin-2.2.0/lib/origin/extensions/boolean.rb0000644000004100000410000000145212665643034021245 0ustar www-datawww-data# encoding: utf-8 module Origin module Extensions # This module contains extensions for boolean selection. module Boolean module ClassMethods # Evolve the value into a boolean value stored in MongoDB. Will return # true for any of these values: true, t, yes, y, 1, 1.0. # # @example Evolve the value to a boolean. # Boolean.evolve(true) # # @param [ Object ] The object to evolve. # # @return [ true, false ] The boolean value. # # @since 1.0.0 def evolve(object) __evolve__(object) do |obj| obj.to_s =~ (/(true|t|yes|y|1|1.0)$/i) ? true : false end end end end end end ::Boolean.__send__(:extend, Origin::Extensions::Boolean::ClassMethods) origin-2.2.0/lib/origin/extensions/array.rb0000644000004100000410000001106312665643034020743 0ustar www-datawww-data# encoding: utf-8 module Origin module Extensions # The array module adds custom behaviour for Origin onto the Array class. module Array # Combine the two objects using the add strategy. # # @example Add the object to the array. # [ 1, 2, 3 ].__add__(4) # # @param [ Object ] object The object to add. # # @return [ Object ] The result of the add. # # @since 1.0.0 def __add__(object) object.__add_from_array__(self) end # Return the object as an array. # # @example Get the array. # [ 1, 2 ].__array__ # # @return [ Array ] self # # @since 1.0.0 def __array__; self; end # Makes a deep copy of the array, deep copying every element inside the # array. # # @example Get a deep copy of the array. # [ 1, 2, 3 ].__deep_copy__ # # @return [ Array ] The deep copy of the array. # # @since 1.0.0 def __deep_copy__ map { |value| value.__deep_copy__ } end # Evolve the array into an array of mongo friendly dates. (Times at # midnight). # # @example Evolve the array to dates. # [ Date.new(2010, 1, 1) ].__evolve_date__ # # @return [ Array