diff --git a/lib/dolly/collection.rb b/lib/dolly/collection.rb index 3dc7dfa..0bdecc6 100644 --- a/lib/dolly/collection.rb +++ b/lib/dolly/collection.rb @@ -56,9 +56,20 @@ def rows= ary end end + def doc_rows= ary + ary.each do |doc| + id = doc.delete '_id' + rev = doc.delete '_rev' if doc['_rev'] + document = (docs_class || doc_class(id)).new doc + document.doc = doc.merge({'_id' => id, '_rev' => rev}) + self << document + end + end + def load parsed = JSON::parse json - self.rows = parsed['rows'] + self.rows = parsed['rows'] if parsed['rows'] + self.doc_rows = parsed['docs'] if parsed['docs'] end def to_json options = {} diff --git a/lib/dolly/document.rb b/lib/dolly/document.rb index ddfc5ff..868848b 100644 --- a/lib/dolly/document.rb +++ b/lib/dolly/document.rb @@ -1,6 +1,7 @@ require "dolly/query" require "dolly/property" require 'dolly/timestamps' +require "dolly/mango" module Dolly class Document @@ -9,7 +10,7 @@ class Document extend Dolly::Timestamps attr_accessor :rows, :doc, :key - class_attribute :properties + class_attribute :properties, :mango_scopes cattr_accessor :timestamps do {} end @@ -149,6 +150,27 @@ def self.property *ary end end + class << self + def mango_scope scope_name, scope + self.mango_scopes ||= {} + name = scope_name.to_sym + self.mango_scopes[name] = ->(query_object, args=nil) { Mango::Scope.new(query_object, scope, args) } + + (class << self; self end).instance_eval do + define_method name do |*args| + self.mango_scopes[name].call(Mango::Query.new(self), *args) + end + end + end + + def selector name, *operator, value + anonymous_scope = ->{ selector(name, *operator, value) } + query_object = Mango::Query.new(self) + args = nil + Mango::Scope.new query_object, anonymous_scope, args + end + end + private #TODO: create a PropertiesSet service object, to do all this def self.write_methods name diff --git a/lib/dolly/mango.rb b/lib/dolly/mango.rb new file mode 100644 index 0000000..1efe106 --- /dev/null +++ b/lib/dolly/mango.rb @@ -0,0 +1,10 @@ +require 'dolly/mango/query_validator' +require 'dolly/mango/selector' +require 'dolly/mango/query' +require 'dolly/mango/scope' + +module Dolly + module Mango + + end +end diff --git a/lib/dolly/mango/query.rb b/lib/dolly/mango/query.rb new file mode 100644 index 0000000..9fa1df4 --- /dev/null +++ b/lib/dolly/mango/query.rb @@ -0,0 +1,38 @@ +module Dolly + module Mango + class Query + include Dolly::Mango::Selector + + FIELDS_KEY = 'fields'.freeze + LIMIT_KEY = 'limit'.freeze + SKIP_KEY = 'skip'.freeze + SORT_KEY = 'sort'.freeze + + attr_reader :proxy_class, :query + + def initialize proxy_class + @proxy_class = proxy_class + @query = Hash.new{ |h,k| h[k] = Hash.new(&h.default_proc) } + query.compare_by_identity + end + + def limit value + query[LIMIT_KEY] = value + end + + def sort name, operator + query[SORT_KEY] ||= [] + query[SORT_KEY] << {name => operator} + end + + def fields *fields + query[FIELDS_KEY] ||= [] + query[FIELDS_KEY].push *fields + end + + def skip integer + query[SKIP_KEY] = integer.to_i + end + end + end +end diff --git a/lib/dolly/mango/query_validator.rb b/lib/dolly/mango/query_validator.rb new file mode 100644 index 0000000..e5ff95d --- /dev/null +++ b/lib/dolly/mango/query_validator.rb @@ -0,0 +1,52 @@ +module Dolly + module Mango + class QueryValidator + + def initialize operator, value + @operator, @value = operator, value + end + + def validate! + return if Selector::EQUALITY_OPERATORS.include? operator + raise Dolly::BadQueryArguement.new(operator, 'Boolean') if boolean_op? + raise Dolly::BadQueryArguement.new(operator, Selector::POSSIBLE_TYPE_VALUES.join(', ')) if type_op? + raise Dolly::BadQueryArguement.new(operator, Array) if array_op? + raise Dolly::BadQueryArguement.new(operator, Integer) if int_op? + raise Dolly::BadQueryArguement.new(operator, '[Divisor, Remainder] Array of Integers') if mod_op? + raise Dolly::BadQueryArguement.new(operator, String) if regex_op? + raise Dolly::BadQueryArguement.new(operator, Array) if combination_op? + end + + private + attr_reader :operator, :value + + def boolean_op? + operator == Selector::EXISTS_OPERATOR && ![true, false].include?(value) + end + + def type_op? + operator == Selector::TYPE_OPERATOR && !Selector::POSSIBLE_TYPE_VALUES.include?(value) + end + + def array_op? + Selector::ARRAY_OPERATORS.include?(operator) && !value.is_a?(Array) + end + + def int_op? + operator == Selector::SIZE_OPERATOR && !value.is_a?(Integer) + end + + def mod_op? + operator == Selector::MOD_OPERATOR && (!value.is_a?(Array) || value.count != 2 || value.none? {|el| el.is_a? Integer }) + end + + def regex_op? + operator == Selector::REGEX_OPERATOR && !value.is_a?(String) + end + + def combination_op? + Selector::COMBINATION_OPERATORS.include?(operator) && !value.is_a?(Array) + end + end + end +end diff --git a/lib/dolly/mango/scope.rb b/lib/dolly/mango/scope.rb new file mode 100644 index 0000000..1dfb174 --- /dev/null +++ b/lib/dolly/mango/scope.rb @@ -0,0 +1,33 @@ +module Dolly + module Mango + class Scope + attr_reader :query_object, :scope, :scope_args + + delegate :proxy_class, :query, to: :query_object + + def initialize query_object, scope, scope_args + @query_object, @scope, @scope_args = query_object, scope, scope_args + evaluate_scope + end + + def method_missing(method, *args, &block) + if proxy_class.mango_scopes.include?(method) + proxy_class.mango_scopes[method].call(query_object, args) + elsif query_object.respond_to? method + query_object.send(method, *args, &block) + return self + else + resp = proxy_class.database.mango query.to_json + collection = Dolly::Collection.new(resp.response.body, proxy_class) + collection.send(method, *args, &block) + end + end + + private + + def evaluate_scope + query_object.instance_exec(*scope_args, &scope) + end + end + end +end diff --git a/lib/dolly/mango/selector.rb b/lib/dolly/mango/selector.rb new file mode 100644 index 0000000..64cb260 --- /dev/null +++ b/lib/dolly/mango/selector.rb @@ -0,0 +1,125 @@ +module Dolly + module Mango + module Selector + + SELECTOR = 'selector'.freeze + + # Equality Operators + EQ_OPERATOR = '$eq'.freeze + NE_OPERATOR = '$ne'.freeze + GT_OPERATOR = '$gt'.freeze + LT_OPERATOR = '$lt'.freeze + GTE_OPERATOR = '$gte'.freeze + LTE_OPERATOR = '$lte'.freeze + + EQUALITY_OPERATORS = [ EQ_OPERATOR, NE_OPERATOR, GT_OPERATOR, LT_OPERATOR, GTE_OPERATOR, LTE_OPERATOR ].freeze + + # Object Operators + EXISTS_OPERATOR = '$exists'.freeze + TYPE_OPERATOR = '$type'.freeze + + OBJECT_OPERATORS = [EXISTS_OPERATOR, TYPE_OPERATOR].freeze + + # Array Operators + IN_OPERATOR = '$in'.freeze + NIN_OPERATOR = '$nin'.freeze + SIZE_OPERATOR = '$size'.freeze + + ARRAY_OPERATORS = [IN_OPERATOR, NIN_OPERATOR].freeze + + # Miscellaneous Operators + MOD_OPERATOR = '$mod'.freeze + REGEX_OPERATOR = '$regex'.freeze + + MISC_OPERATORS = [MOD_OPERATOR, REGEX_OPERATOR].freeze + + # Combination Operators + OR_OPERATOR = '$or'.freeze + NOT_SELECTOR = '$not'.freeze + NOR_SELECTOR = '$nor'.freeze + AND_OPERATOR = '$and'.freeze + ALL_OPERATOR = '$all'.freeze + EM_OPERATOR = '$elemMatch'.freeze + + COMBINATION_OPERATORS = [ OR_OPERATOR, NOT_SELECTOR, NOR_SELECTOR, AND_OPERATOR, ALL_OPERATOR, EM_OPERATOR ].freeze + + POSSIBLE_TYPE_VALUES = %w/null boolean number string array object/.freeze + + ALL_OPERATORS = [ EQUALITY_OPERATORS, OBJECT_OPERATORS, ARRAY_OPERATORS, SIZE_OPERATOR, MISC_OPERATORS, COMBINATION_OPERATORS ].flatten.freeze + + def selector name, *operator, value + proxy_operator = operator.last + operator_check! proxy_operator + operator = operator.count > 1 ? operator : operator.first + select_operator_map[operator].call(name, value) + end + + private + + def select_operator_map + { + eq: ->(name, value) { build_equality_selector name, value, EQ_OPERATOR }, + ne: ->(name, value) { build_equality_selector name, value, NE_OPERATOR }, + gt: ->(name, value) { build_equality_selector name, value, GT_OPERATOR }, + gte: ->(name, value) { build_equality_selector name, value, GTE_OPERATOR }, + lt: ->(name, value) { build_equality_selector name, value, LT_OPERATOR }, + lte: ->(name, value) { build_equality_selector name, value, LTE_OPERATOR }, + + exists: ->(name, value=true) { build_equality_selector name, value, EXISTS_OPERATOR }, + type: ->(name, value) { build_equality_selector name, value, TYPE_OPERATOR }, + + in: ->(name, value) { build_equality_selector name, value, IN_OPERATOR }, + nin: ->(name, value) { build_equality_selector name, value, NIN_OPERATOR }, + size: ->(name, value) { build_equality_selector name, value, SIZE_OPERATOR }, + + mod: ->(name, value) { build_equality_selector name, value, MOD_OPERATOR }, + regex: ->(name, value) { build_equality_selector name, value, REGEX_OPERATOR }, + + nor: ->(name, value) { build_exclusive_selector name, value, NOR_SELECTOR} , + all: ->(name, value) { build_exclusive_selector name, value, ALL_OPERATOR }, + and: ->(name, value) { build_exclusive_selector name, value, AND_OPERATOR }, + or: ->(name, value) { build_exclusive_selector name, value, OR_OPERATOR }, + + [:em, :gt] => ->(name, value) { build_composite_selector name, value, EM_OPERATOR, GT_OPERATOR }, + [:em, :gte] => ->(name, value) { build_composite_selector name, value, EM_OPERATOR, GTE_OPERATOR }, + [:em, :lt] => ->(name, value) { build_composite_selector name, value, EM_OPERATOR, LT_OPERATOR }, + [:em, :lte] => ->(name, value) { build_composite_selector name, value, EM_OPERATOR, LTE_OPERATOR }, + [:em, :or] => ->(name, value) { build_composite_selector name, value, EM_OPERATOR, OR_OPERATOR }, + [:em, :and] => ->(name, value) { build_composite_selector name, value, EM_OPERATOR, AND_OPERATOR }, + + [:not, :gt] => ->(name, value) { build_composite_selector name, value, NOT_OPERATOR, GT_OPERATOR }, + [:not, :gte] => ->(name, value) { build_composite_selector name, value, NOT_OPERATOR, GTE_OPERATOR }, + [:not, :lt] => ->(name, value) { build_composite_selector name, value, NOT_OPERATOR, LT_OPERATOR }, + [:not, :lte] => ->(name, value) { build_composite_selector name, value, NOT_OPERATOR, LTE_OPERATOR }, + [:not, :or] => ->(name, value) { build_composite_selector name, value, NOT_OPERATOR, OR_OPERATOR }, + [:not, :and] => ->(name, value) { build_composite_selector name, value, NOT_OPERATOR, AND_OPERATOR } + }.freeze + end + + def build_equality_selector name, value, operator + operator_value_type_check!(operator, value) + query[SELECTOR][name][operator] = value + end + + def build_exclusive_selector name, value, operator + operator_value_type_check!(operator, value) + query[SELECTOR][operator][name] = value + end + + def build_composite_selector name, value, *operators + first, second = operators + operator_value_type_check!(second, value) + + query[SELECTOR][name][first][second] = value + end + + def operator_check! operator + raise Dolly::UnrecognizedOperator.new(operator) unless select_operator_map.keys.include? operator + end + + def operator_value_type_check! operator, value + QueryValidator.new(operator, value).validate! + end + end + end +end diff --git a/lib/dolly/query.rb b/lib/dolly/query.rb index 609b7fc..3135e05 100644 --- a/lib/dolly/query.rb +++ b/lib/dolly/query.rb @@ -51,7 +51,7 @@ def last limit = 1 def build_collection q res = database.all_docs(q) - Collection.new res.response.body, name_for_class + Collection.new res.parsed_response.to_json, name_for_class end def find_with doc, view_name, opts = {} diff --git a/lib/dolly/request.rb b/lib/dolly/request.rb index d6ddaaf..08877ee 100644 --- a/lib/dolly/request.rb +++ b/lib/dolly/request.rb @@ -7,6 +7,7 @@ class Request include HTTParty DEFAULT_HOST = 'localhost' DEFAULT_PORT = '5984' + MANGO_QUERY = '_find'.freeze attr_accessor :database_name, :host, :port, :bulk_document @@ -44,6 +45,10 @@ def delete resource request :delete, full_path(resource), {} end + def mango data + request :post, full_path(MANGO_QUERY), {body: data} + end + def attach resource, attachment_name, data, headers = {} data = StringIO.new(data) if data.is_a?(String) request :put, attachment_path(resource, attachment_name), {body: data, headers: headers} @@ -59,7 +64,7 @@ def uuids opts = {} def all_docs data = {} data = values_to_json data.merge( include_docs: true ) - request :get, full_path('_all_docs'), {query: data} + request(:get, full_path('_all_docs'), { query: data }) end def request method, resource, data = nil diff --git a/lib/exceptions/dolly.rb b/lib/exceptions/dolly.rb index 46e70a2..c810bcd 100644 --- a/lib/exceptions/dolly.rb +++ b/lib/exceptions/dolly.rb @@ -35,4 +35,22 @@ def to_s end class DocumentInvalidError < RuntimeError; end class MissingPropertyError < RuntimeError; end + class BadQueryArguement < RuntimeError + def initialize operator, expected_type + @operator, @expected_type = operator, expected_type + end + + def to_s + "The operator #{@operator} only accepts a(n) #{@expected_type}" + end + end + class UnrecognizedOperator < RuntimeError + def initialize operator + @operator = operator + end + + def to_s + "The operator #{@operator} is unrecognized" + end + end end diff --git a/test/document_test.rb b/test/document_test.rb index f1c9cfe..fbfda93 100644 --- a/test/document_test.rb +++ b/test/document_test.rb @@ -62,6 +62,8 @@ def setup all_docs = [ {foo: 'Foo B', bar: 'Bar B', type: 'foo_bar'}, {foo: 'Foo A', bar: 'Bar A', type: 'foo_bar'}] view_resp = build_view_response [data] + uuid_resp = {"uuids" => ["ec68ef07faf8157e568b0913e74b0e1a"]} + empty_resp = build_view_response [] not_found_resp = generic_response [{ key: "foo_bar/2", error: "not_found" }] @multi_resp = build_view_response all_docs @@ -72,17 +74,18 @@ def setup build_request [["foo_bar","1"],["foo_bar","2"]], @multi_resp #TODO: Mock Dolly::Request to return helper with expected response. request builder can be tested by itself. - FakeWeb.register_uri :get, "#{query_base_path}?startkey=%22foo_bar%2F%22&endkey=%22foo_bar%2F%EF%BF%B0%22&include_docs=true", body: @multi_resp.to_json - FakeWeb.register_uri :get, "#{query_base_path}?startkey=%22foo_bar%2F%22&endkey=%22foo_bar%2F%EF%BF%B0%22&limit=1&include_docs=true", body: view_resp.to_json - FakeWeb.register_uri :get, "#{query_base_path}?endkey=%22foo_bar%2F%22&startkey=%22foo_bar%2F%EF%BF%B0%22&limit=1&descending=true&include_docs=true", body: view_resp.to_json - FakeWeb.register_uri :get, "#{query_base_path}?startkey=%22foo_bar%2F%22&endkey=%22foo_bar%22%2C%7B%7D&limit=2&include_docs=true", body: @multi_resp.to_json - FakeWeb.register_uri :get, "#{query_base_path}?endkey=%22foo_bar%2F%22&startkey=%22foo_bar%2F%EF%BF%B0%22&limit=2&descending=true&include_docs=true", body: @multi_resp.to_json - FakeWeb.register_uri :get, "#{query_base_path}?keys=%5B%22foo_bar%2F1%22%5D&include_docs=true", body: view_resp.to_json - FakeWeb.register_uri :get, "#{query_base_path}?keys=%5B%5D&include_docs=true", body: not_found_resp.to_json - FakeWeb.register_uri :get, "#{query_base_path}?keys=%5B%22foo_bar%2Ferror%22%5D&include_docs=true", body: 'error', status: ["500", "Error"] - FakeWeb.register_uri :get, "#{query_base_path}?keys=%5B%22foo_bar%2F1%22%2C%22foo_bar%2F2%22%5D&include_docs=true", body: @multi_resp.to_json - FakeWeb.register_uri :get, "#{query_base_path}?keys=%5B%22foo_bar%2F2%22%5D&include_docs=true", body: not_found_resp.to_json - FakeWeb.register_uri :get, "#{query_base_path}?keys=%5B%22foo_bar%2Fbig_doc%22%5D&include_docs=true", body: build_view_response([data.merge(other_property: 'other')]).to_json + FakeWeb.register_uri :get, "#{query_base_path}?startkey=%22foo_bar%2F%22&endkey=%22foo_bar%2F%EF%BF%B0%22&include_docs=true", body: @multi_resp.to_json, content_type: "application/json" + FakeWeb.register_uri :get, "#{query_base_path}?startkey=%22foo_bar%2F%22&endkey=%22foo_bar%2F%EF%BF%B0%22&limit=1&include_docs=true", body: view_resp.to_json, content_type: "application/json" + FakeWeb.register_uri :get, "#{query_base_path}?endkey=%22foo_bar%2F%22&startkey=%22foo_bar%2F%EF%BF%B0%22&limit=1&descending=true&include_docs=true", body: view_resp.to_json, content_type: "application/json" + FakeWeb.register_uri :get, "#{query_base_path}?startkey=%22foo_bar%2F%22&endkey=%22foo_bar%22%2C%7B%7D&limit=2&include_docs=true", body: @multi_resp.to_json, content_type: "application/json" + FakeWeb.register_uri :get, "#{query_base_path}?endkey=%22foo_bar%2F%22&startkey=%22foo_bar%2F%EF%BF%B0%22&limit=2&descending=true&include_docs=true", body: @multi_resp.to_json, content_type: "application/json" + FakeWeb.register_uri :get, "#{query_base_path}?keys=%5B%22foo_bar%2F1%22%5D&include_docs=true", body: view_resp.to_json, content_type: "application/json" + FakeWeb.register_uri :get, "#{query_base_path}?keys=%5B%5D&include_docs=true", body: not_found_resp.to_json, content_type: "application/json" + FakeWeb.register_uri :get, "#{query_base_path}?keys=%5B%22foo_bar%2Ferror%22%5D&include_docs=true", body: 'error', status: ["500", "Error"], content_type: "application/json" + FakeWeb.register_uri :get, "#{query_base_path}?keys=%5B%22foo_bar%2F1%22%2C%22foo_bar%2F2%22%5D&include_docs=true", body: @multi_resp.to_json, content_type: "application/json" + FakeWeb.register_uri :get, "#{query_base_path}?keys=%5B%22foo_bar%2F2%22%5D&include_docs=true", body: not_found_resp.to_json, content_type: "application/json" + FakeWeb.register_uri :get, "#{query_base_path}?keys=%5B%22foo_bar%2Fbig_doc%22%5D&include_docs=true", body: build_view_response([data.merge(other_property: 'other')]).to_json, content_type: "application/json" + FakeWeb.register_uri :get, "http://localhost:5984/_uuids", body: uuid_resp.to_json, content_type: "application/json" end test 'new in memory document' do @@ -182,7 +185,7 @@ def setup test 'reload reloads the doc attribute from database' do assert foo = FooBar.find('1') expected_doc = foo.doc.dup - FakeWeb.register_uri :get, "#{query_base_path}?keys=%5B%22foo_bar%2F0%22%5D&include_docs=true", body: build_view_response([expected_doc]).to_json + FakeWeb.register_uri :get, "#{query_base_path}?keys=%5B%22foo_bar%2F0%22%5D&include_docs=true", body: build_view_response([expected_doc]).to_json, content_type: "application/json" assert foo.foo = 1 assert_not_equal expected_doc, foo.doc assert foo.reload @@ -196,7 +199,7 @@ def setup assert foo.foo = 1 assert foo.save assert expected_doc = foo.doc - FakeWeb.register_uri :get, "#{query_base_path}?keys=%5B%22foo_bar%2F0%22%5D&include_docs=true", body: build_view_response([expected_doc]).to_json + FakeWeb.register_uri :get, "#{query_base_path}?keys=%5B%22foo_bar%2F0%22%5D&include_docs=true", body: build_view_response([expected_doc]).to_json, content_type: "application/json" assert foo.reload assert_equal 1, foo.foo end @@ -305,19 +308,6 @@ def setup assert_equal "foo_bar/b", bar.id end - test 'new document with no id' do - foo = FooBar.new - uuid = %r{ - \A - foo_bar / - \h{8} # 8 hex chars - (?: - \h{4} ){3} # 3 groups of 4 hex chars (hyphen sep) - - \h{12} # 12 hex chars (hyphen sep again) - \Z - }x - assert foo.id.match(uuid) - end - test 'update document properties' do foo = FooBar.new 'id' => 'a', foo: 'ab', bar: 'ba' assert_equal 'ab', foo.foo diff --git a/test/mango/query_validator_test.rb b/test/mango/query_validator_test.rb new file mode 100644 index 0000000..481aab1 --- /dev/null +++ b/test/mango/query_validator_test.rb @@ -0,0 +1,77 @@ +require 'test_helper' + +class QueryTestDoc < Dolly::Document; end + +class QueryValidatorTest < ActiveSupport::TestCase + setup do + @query_object = Dolly::Mango::Query.new(QueryTestDoc) + end + + class UnrecognizedOperatorTest < QueryValidatorTest + test 'Dolly::UnrecognizedOperator is raised if the operator is unknown' do + assert_raise Dolly::UnrecognizedOperator do + @query_object.selector('field', :operator, "value") + end + end + end + + class AcceptedValuesTest < QueryValidatorTest + test 'nothing is raised if an equality operator is invoked' do + assert_nothing_raised do + @query_object.selector('field', :eq, "value") + @query_object.selector('ne_field', :ne, 1) + @query_object.selector('gt_field', :gt, ["value"]) + @query_object.selector('gte_field', :gte, 2008) + @query_object.selector('lt_field', :lt, "value") + @query_object.selector('lte_field', :lte, {'key'=>'value'}) + end + end + + test 'nothing is raised if exists operator is invoked with a boolean' do + assert_nothing_raised do + @query_object.selector('field', :exists, true) + @query_object.selector('field', :exists, false) + end + end + + test 'nothing is raised if type operator is invoked with null, boolean, number, string, array or and object' do + assert_nothing_raised do + @query_object.selector('field', :type, 'null') + @query_object.selector('field', :type, 'boolean') + @query_object.selector('field', :type, 'number') + @query_object.selector('field', :type, 'string') + @query_object.selector('field', :type, 'array') + @query_object.selector('field', :type, 'object') + end + end + + test 'nothing is raised if an array operator is invoked with the correct values' do + assert_nothing_raised do + @query_object.selector('field', :in, ['value']) + @query_object.selector('field', :nin, ['value']) + @query_object.selector('field', :size, 1) + end + end + + test 'nothing is raised if a misc operator is invoked with the correct values' do + assert_nothing_raised do + @query_object.selector('field', :mod, [3,1]) + @query_object.selector('field', :regex, /[a-zA-Z]{1}/.to_s) + end + end + end + + class UnacceptedValuesTest < QueryValidatorTest + test 'Dolly::BadQueryArguement is raised when an operatore is invoked with the wrong arguements' do + assert_raise Dolly::BadQueryArguement do + @query_object.selector('field', :exists, 1) + @query_object.selector('field', :type, 1) + @query_object.selector('field', :in, 1) + @query_object.selector('field', :nin, 1) + @query_object.selector('field', :size, 'value') + @query_object.selector('field', :mod, [3,"1"]) + @query_object.selector('field', :regex, [""]) + end + end + end +end diff --git a/test/mango_document_test.rb b/test/mango_document_test.rb new file mode 100644 index 0000000..3e31c08 --- /dev/null +++ b/test/mango_document_test.rb @@ -0,0 +1,160 @@ +require 'test_helper' + +class MangoDoc < Dolly::Document + property :year, :title, :char + property :visible_to, class_name: Hash, default: Hash.new + property :default_collection, class_name: Array, default: Array.new + timestamps! + + mango_scope :by_year, ->(year) { selector('year', :eq, year) } + mango_scope :by_title, ->(title) { selector('title', :eq, title) } + mango_scope :by_char, ->(char) { selector('char', :eq, char) } + mango_scope :recent, -> { selector('created_at', :gt, 1.year.ago.to_s )} + mango_scope :old, -> { selector('created_at', :lt, 1.year.ago.to_s )} + mango_scope :by_visible_to_schools, -> (school_id) { selector('visible_to.schools', :eq, school_id) } + mango_scope :collection_items_greater_than, -> (item) { selector('default_collection', :em, :gt, item)} + + mango_scope :id_greater_than, ->(id=nil) { selector('_id', :gt, id) } + mango_scope :support_date_greater_than, ->(date) { selector('support_date', :gt, date)} + mango_scope :generic_elematch_or, ->(field, item) { selector(field, :em, :or, item) } +end + +class SecondMangoDoc < Dolly::Document + property :name + + mango_scope :by_name, ->(name) { selector('name', :eq, name) } +end + +class MangoDocumentTest < ActiveSupport::TestCase + class ScopeInterfaceTest < MangoDocumentTest + test 'SecondMangoDoc does not have the scopes of MangoDoc' do + assert_not_same MangoDoc.mango_scopes.keys, SecondMangoDoc.mango_scopes.keys + end + + test 'responds to the scoped method' do + MangoDoc.mango_scopes.keys.each do |k| + assert_respond_to MangoDoc, k + end + end + end + + class QueryIsBuiltTest < MangoDocumentTest + test 'calling the scope builds the select query' do + value = 2000 + query = MangoDoc.by_year(value).query.to_json + expected = {"selector"=>{"year"=>{"$eq"=>value}}}.to_json + + assert_equal expected, query + end + + test 'the scopes are chainable' do + title_value = 'A' + year_value = 2000 + query = MangoDoc.by_title(title_value).by_year(year_value).query.to_json + expected = {"selector"=>{"title"=>{"$eq"=>title_value}, "year"=>{"$eq"=>year_value}}}.to_json + + assert_equal expected, query + end + + test 'the scopes are chainable2' do + title_value = 'A' + year_value = 2000 + char_value = 'B' + query = MangoDoc.by_title(title_value).by_year(year_value).by_char(char_value).query.to_json + expected = {"selector"=>{"title"=>{"$eq"=>title_value}, "year"=>{"$eq"=>year_value}, "char"=>{"$eq"=>char_value}}}.to_json + + assert_equal expected, query + end + + test 'greater than selector builds query correctly' do + query = MangoDoc.by_title('A').recent.query.to_json + expected = {"selector"=>{"title"=>{"$eq"=>"A"}, "created_at"=>{"$gt"=>1.year.ago.to_s}}}.to_json + + assert_equal expected, query + end + + test 'less than selector builds query correctly' do + query = MangoDoc.by_title('A').old.query.to_json + expected = {"selector"=>{"title"=>{"$eq"=>"A"}, "created_at"=>{"$lt"=>1.year.ago.to_s}}}.to_json + + assert_equal expected, query + end + + test 'selector can handle nested json object querys' do + value = "some_id" + query = MangoDoc.by_visible_to_schools(value).query.to_json + expected = {"selector"=>{"visible_to.schools"=>{"$eq"=>value}}}.to_json + assert_equal expected, query + end + + test 'selector can handle multiple operators' do + query = MangoDoc.collection_items_greater_than(1).query.to_json + expected = {"selector"=>{"default_collection"=>{"$elemMatch"=>{"$gt"=>1}}}}.to_json + assert_equal expected, query + end + + test 'an anonymous selector may be chained to a scope' do + year = 2000 + title = 'Bond' + query = MangoDoc.by_year(year).selector('title', :eq, title).query.to_json + expected = {"selector"=>{"year"=>{"$eq"=>2000}, "title"=>{"$eq"=>"Bond"}}}.to_json + assert_equal expected, query + end + + test 'an anonymous selector may be chained to the class' do + title = 'Bond' + query = MangoDoc.selector('title', :eq, title).query.to_json + expected = {"selector"=>{"title"=>{"$eq"=>"Bond"}}}.to_json + assert_equal expected, query + end + + test 'an mango_scope can be chained to an anonymous selector' do + year = 2000 + title = 'Bond' + query = MangoDoc.selector('title', :eq, title).by_year(year).query.to_json + expected = {"selector"=>{"title"=>{"$eq"=>"Bond"}, "year"=>{"$eq"=>2000}}}.to_json + assert_equal expected, query + end + + test 'complex selector can be built' do + expected_query = { + "selector" => { + "_id" => { + "$gt" => nil + }, + "support_type_date" => { + "$gt" => Date.today.to_s(:db) + }, + "visible_to.schools" => { + "$elemMatch" => { + "$or" => ([] << "") + } + }, + "visible_to.countries" => { + "$elemMatch" => { + "$or" => ([] << "") + } + }, + "visible_to.programs" => { + "$elemMatch" => { + "$or" => ([] << "") + } + }, + "visible_to.grades" => { + "$elemMatch" => { + "$or" => ([] << "") + } + } + } + }.to_json + query = MangoDoc + .id_greater_than(nil) + .support_date_greater_than(Date.today.to_s(:db)) + .generic_elematch_or('visible_to.schools', [] << "") + .generic_elematch_or('visible_to.countries', [] << "") + .generic_elematch_or('visible_to.programs', [] << "") + .generic_elematch_or('visible_to.grades', [] << "").query.to_json + assert expected_query, query + end + end +end