ddr-models 2.4.0.rc4 → 2.4.0.rc5

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,17 +1,42 @@
1
+ require "virtus"
2
+
1
3
  module Ddr::Index
2
4
  class QueryClause
5
+ include Virtus.value_object
6
+
7
+ ANY_FIELD = Field.new('*').freeze
8
+ ANY_VALUE = "[* TO *]"
9
+ QUOTE = '"'
10
+
11
+ TERM_QUERY = "{!term f=%{field}}%{value}"
12
+ STANDARD_QUERY = "%{field}:%{value}"
13
+ NEGATIVE_QUERY = "-%{field}:%{value}"
14
+ DISJUNCTION = "{!lucene q.op=OR df=%{field}}%{value}"
15
+
16
+ values do
17
+ attribute :field, FieldAttribute
18
+ attribute :value, String
19
+ attribute :quote_value, Boolean, default: false
20
+ attribute :template, String, default: STANDARD_QUERY
21
+ end
22
+
23
+ def to_s
24
+ template % { field: field, value: quote_value ? quote(value) : value }
25
+ end
3
26
 
4
- PRESENT = "[* TO *]"
5
- TERM = "{!term f=%s}%s"
6
- BEFORE_DAYS = "[* TO NOW-%sDAYS]"
27
+ def quote(value)
28
+ self.class.quote(value)
29
+ end
7
30
 
8
31
  class << self
9
- # Builds a standard query clause, no escaping applied.
10
- # @param field [Field, String] field
11
- # @param value [String] query value
12
- # @return [String] query clause
13
- def build(field, value)
14
- [field, value].join(":")
32
+
33
+ def quote(value)
34
+ # Derived from Blacklight::Solr::SearchBuilderBehavior#solr_param_quote
35
+ unless value =~ /\A[a-zA-Z0-9$_\-\^]+\z/
36
+ QUOTE + value.gsub("'", "\\\\\'").gsub('"', "\\\\\"") + QUOTE
37
+ else
38
+ value
39
+ end
15
40
  end
16
41
 
17
42
  # Builds a query clause to retrieve the index document by unique key.
@@ -20,62 +45,51 @@ module Ddr::Index
20
45
  end
21
46
  alias_method :id, :unique_key
22
47
 
48
+ def where(field, value)
49
+ if value.respond_to?(:each)
50
+ disjunction(field, value)
51
+ else
52
+ new(field: field, value: value, quote_value: true)
53
+ end
54
+ end
55
+
23
56
  # Builds a query clause to filter where field does not have the given value.
24
- # @param field [Field, String] field
25
- # @param value [String] query value
26
- # @return [String] query clause
27
57
  def negative(field, value)
28
- build("-#{field}", value)
58
+ new(field: field, value: value, template: NEGATIVE_QUERY, quote_value: true)
29
59
  end
30
60
 
31
61
  # Builds a query clause to filter where field is present (i.e, has any value)
32
- # @param field [Field, String] field
33
- # @return [String] query clause
34
62
  def present(field)
35
- build(field, PRESENT)
63
+ new(field: field, value: ANY_VALUE)
36
64
  end
37
65
 
38
66
  # Builds a query clause to filter where field is NOT present (no values)
39
- # @param field [Field, String] field
40
- # @return [String] query clause
41
67
  def absent(field)
42
- negative(field, PRESENT)
68
+ new(field: "-#{field}", value: ANY_VALUE)
43
69
  end
44
70
 
45
71
  # Builds a query clause to filter where field contains at least one of a set of values.
46
- # @param field [Field, String] field
47
- # @param values [Array<String>] query values
48
- # @return [String] query clause
49
- def or_values(field, values)
50
- build(field, QueryValue.or_values(values))
72
+ def disjunction(field, values)
73
+ value = values.map { |v| quote(v) }.join(" ")
74
+ new(field: field, value: value, template: DISJUNCTION)
51
75
  end
52
76
 
53
77
  # Builds a query clause to filter where date field value is earlier than a date/time value.
54
- # @param field [Field, String] field
55
- # @param value [Object] query value, must be coercible to a Solr date string.
56
- # @return [String] query clause
57
- def before(field, date_time)
58
- value = "[* TO %s]" % Ddr::Utils.solr_date(date_time)
59
- build(field, value)
78
+ def before(field, value)
79
+ new(field: field, value: "[* TO %s]" % Ddr::Utils.solr_date(value))
60
80
  end
81
+ alias_method :before_date_time, :before
61
82
 
62
83
  # Builds a query clause to filter where date field value is earlier than a number of days before now.
63
- # @param field [Field, String] field
64
- # @param value [String, Fixnum] query value, must be coercible to integer.
65
- # @return [String] query clause
66
- def before_days(field, days)
67
- value = BEFORE_DAYS % days.to_i
68
- build(field, value)
84
+ def before_days(field, value)
85
+ new(field: field, value: "[* TO NOW-%iDAYS]" % value)
69
86
  end
70
87
 
71
88
  # Builds a "term query" clause to filter where field contains value.
72
- # Double quotes are escaped.
73
- # @param field [Field, String] field
74
- # @param value [String] query value
75
- # @return [String] Solr term query
76
89
  def term(field, value)
77
- TERM % [field, value.gsub(/"/, '\"')]
90
+ new(field: field, value: value, template: TERM_QUERY)
78
91
  end
92
+
79
93
  end
80
94
 
81
95
  end
@@ -1,23 +1,39 @@
1
- require "delegate"
2
-
3
1
  module Ddr::Index
4
- class QueryParams < SimpleDelegator
2
+ class QueryParams
5
3
 
6
- attr_reader :params
4
+ attr_reader :query
7
5
 
8
6
  def initialize(query)
9
- super
10
- @params = {
11
- q: q,
12
- fq: fq,
13
- fl: fields.join(","),
14
- sort: sort.join(","),
7
+ @query = query
8
+ end
9
+
10
+ def params
11
+ { q: q_param,
12
+ fq: filter_queries,
13
+ fl: fields,
14
+ sort: sort,
15
15
  rows: rows,
16
16
  }.select { |k, v| v.present? }
17
17
  end
18
18
 
19
- def fq
20
- filters.map(&:clauses).flatten
19
+ def q_param
20
+ query.q.to_s
21
+ end
22
+
23
+ def filter_queries
24
+ query.filter_clauses.map(&:to_s)
25
+ end
26
+
27
+ def fields
28
+ query.fields.join(",")
29
+ end
30
+
31
+ def sort
32
+ query.sort.join(",")
33
+ end
34
+
35
+ def rows
36
+ query.rows
21
37
  end
22
38
 
23
39
  end
@@ -2,18 +2,26 @@ require "virtus"
2
2
 
3
3
  module Ddr::Index
4
4
  class SortOrder
5
- include Virtus.value_object(strict: true)
5
+ include Virtus.value_object
6
6
 
7
7
  ASC = "asc"
8
8
  DESC = "desc"
9
9
 
10
10
  values do
11
- attribute :field, String
11
+ attribute :field, FieldAttribute
12
12
  attribute :order, String
13
13
  end
14
14
 
15
15
  def to_s
16
- [field, order].join(" ")
16
+ [ field, order ].join(" ")
17
+ end
18
+
19
+ def self.asc(field)
20
+ new(field: field, order: ASC)
21
+ end
22
+
23
+ def self.desc(field)
24
+ new(field: field, order: DESC)
17
25
  end
18
26
 
19
27
  end
@@ -1,5 +1,5 @@
1
1
  module Ddr
2
2
  module Models
3
- VERSION = "2.4.0.rc4"
3
+ VERSION = "2.4.0.rc5"
4
4
  end
5
5
  end
@@ -6,59 +6,101 @@ module Ddr::Index
6
6
  describe "equality" do
7
7
  describe "when the other is a Filter instance" do
8
8
  describe "and the clauses are equal" do
9
- subject { described_class.new(["foo:bar", "spam:eggs"]) }
10
- let(:other) { described_class.new(["foo:bar", "spam:eggs"]) }
9
+ subject { described_class.new(clauses: ["foo:bar", "spam:eggs"]) }
10
+ let(:other) { described_class.new(clauses: ["foo:bar", "spam:eggs"]) }
11
11
  specify { expect(subject).to eq other }
12
12
  end
13
13
  describe "and the clauses are not equal" do
14
- subject { described_class.new(["foo:bar", "bam:baz"]) }
15
- let(:other) { described_class.new(["foo:bar", "spam:eggs"]) }
14
+ subject { described_class.new(clauses: ["foo:bar", "bam:baz"]) }
15
+ let(:other) { described_class.new(clauses: ["foo:bar", "spam:eggs"]) }
16
16
  specify { expect(subject).not_to eq other }
17
17
  end
18
18
  end
19
19
  describe "when the other is not a Filter instance" do
20
- subject { described_class.new(["foo:bar", "spam:eggs"]) }
20
+ subject { described_class.new(clauses: ["foo:bar", "spam:eggs"]) }
21
21
  let(:other) { double(clauses: ["foo:bar", "spam:eggs"]) }
22
22
  specify { expect(subject).not_to eq other }
23
23
  end
24
24
  end
25
25
 
26
26
  describe "class methods" do
27
- describe ".where" do
28
- subject { Filter.where("foo"=>"bar", "spam"=>"eggs", "stuff"=>["dog", "cat", "bird"]) }
29
- its(:clauses) { are_expected.to eq (["{!term f=foo}bar", "{!term f=spam}eggs", "stuff:(dog OR cat OR bird)"]) }
27
+ describe ".is_governed_by" do
28
+ describe "with an object" do
29
+ subject { described_class.is_governed_by(Item.new(pid: "test:1")) }
30
+ its(:clauses) {
31
+ are_expected.to eq([QueryClause.term(:is_governed_by, "info:fedora/test:1")])
32
+ }
33
+ end
34
+ describe "with an ID" do
35
+ subject { described_class.is_governed_by("test:1") }
36
+ its(:clauses) {
37
+ are_expected.to eq([QueryClause.term(:is_governed_by, "info:fedora/test:1")])
38
+ }
39
+ end
30
40
  end
31
- describe "#raw" do
32
- subject { Filter.raw("foo:bar", "spam:eggs") }
41
+ describe ".is_member_of_collection" do
42
+ describe "with an object" do
43
+ subject { described_class.is_member_of_collection(Item.new(pid: "test:1")) }
44
+ its(:clauses) {
45
+ are_expected.to eq([QueryClause.term(:is_member_of_collection, "info:fedora/test:1")])
46
+ }
47
+ end
48
+ describe "with an ID" do
49
+ subject { described_class.is_member_of_collection("test:1") }
50
+ its(:clauses) {
51
+ are_expected.to eq([QueryClause.term(:is_member_of_collection, "info:fedora/test:1")])
52
+ }
53
+ end
54
+ end
55
+ describe ".has_content" do
56
+ subject { described_class.has_content }
57
+ its(:clauses) { are_expected.to eq([QueryClause.where(:active_fedora_model, ["Component", "Attachment", "Target"])]) }
58
+ end
59
+ describe ".where" do
60
+ subject { described_class.where("foo"=>"bar", "spam"=>"eggs", "stuff"=>["dog", "cat", "bird"]) }
61
+ its(:clauses) {
62
+ are_expected.to eq([QueryClause.where("foo", "bar"),
63
+ QueryClause.where("spam", "eggs"),
64
+ QueryClause.where("stuff", ["dog", "cat", "bird"])
65
+ ])
66
+ }
67
+ end
68
+ describe ".raw" do
69
+ subject { described_class.raw("foo:bar", "spam:eggs") }
33
70
  its(:clauses) { are_expected.to eq(["foo:bar", "spam:eggs"]) }
34
71
  end
35
- describe "#negative" do
36
- subject { Filter.negative("foo", "bar") }
37
- its(:clauses) { are_expected.to eq(["-foo:bar"]) }
72
+ describe ".negative" do
73
+ subject { described_class.negative("foo", "bar") }
74
+ its(:clauses) { are_expected.to eq([QueryClause.negative("foo", "bar")]) }
38
75
  end
39
- describe "#present" do
40
- subject { Filter.present("foo") }
41
- its(:clauses) { are_expected.to eq(["foo:[* TO *]"]) }
76
+ describe ".present" do
77
+ subject { described_class.present("foo") }
78
+ its(:clauses) { are_expected.to eq([QueryClause.present("foo")]) }
42
79
  end
43
- describe "#absent" do
44
- subject { Filter.absent("foo") }
45
- its(:clauses) { are_expected.to eq(["-foo:[* TO *]"]) }
80
+ describe ".absent" do
81
+ subject { described_class.absent("foo") }
82
+ its(:clauses) { are_expected.to eq([QueryClause.absent("foo")]) }
46
83
  end
47
- describe "#before_days" do
48
- subject { Filter.before_days("foo", 60) }
49
- its(:clauses) { are_expected.to eq(["foo:[* TO NOW-60DAYS]"]) }
84
+ describe ".before_days" do
85
+ subject { described_class.before_days("foo", 60) }
86
+ its(:clauses) { are_expected.to eq([QueryClause.before_days("foo", 60)]) }
50
87
  end
51
- describe "#before" do
52
- subject { Filter.before("foo", DateTime.parse("Thu, 27 Aug 2015 17:42:34 -0400")) }
53
- its(:clauses) { are_expected.to eq(["foo:[* TO 2015-08-27T21:42:34Z]"]) }
88
+ describe ".before" do
89
+ subject { described_class.before("foo", DateTime.parse("Thu, 27 Aug 2015 17:42:34 -0400")) }
90
+ its(:clauses) {
91
+ are_expected.to eq([QueryClause.before("foo", DateTime.parse("Thu, 27 Aug 2015 17:42:34 -0400"))])
92
+ }
54
93
  end
55
94
  end
56
95
 
57
- describe "instance methods" do
96
+ describe "API methods" do
58
97
  describe "#where" do
59
98
  it "adds raw query filters for the hash of conditions" do
60
99
  subject.where("foo"=>"bar", "spam"=>"eggs", "stuff"=>["dog", "cat", "bird"])
61
- expect(subject.clauses).to eq(["{!term f=foo}bar", "{!term f=spam}eggs", "stuff:(dog OR cat OR bird)"])
100
+ expect(subject.clauses).to eq([QueryClause.where("foo", "bar"),
101
+ QueryClause.where("spam", "eggs"),
102
+ QueryClause.where("stuff", ["dog", "cat", "bird"])
103
+ ])
62
104
  end
63
105
  end
64
106
  describe "#raw" do
@@ -70,31 +112,31 @@ module Ddr::Index
70
112
  describe "#negative" do
71
113
  it "adds a negation query clause" do
72
114
  subject.negative("foo", "bar")
73
- expect(subject.clauses).to eq(["-foo:bar"])
115
+ expect(subject.clauses).to eq([QueryClause.negative("foo", "bar")])
74
116
  end
75
117
  end
76
118
  describe "#present" do
77
119
  it "adds a \"field present\" query clause" do
78
120
  subject.present("foo")
79
- expect(subject.clauses).to eq(["foo:[* TO *]"])
121
+ expect(subject.clauses).to eq([QueryClause.present("foo")])
80
122
  end
81
123
  end
82
124
  describe "#absent" do
83
125
  it "adds a \"field not present\" query clause" do
84
126
  subject.absent("foo")
85
- expect(subject.clauses).to eq(["-foo:[* TO *]"])
127
+ expect(subject.clauses).to eq([QueryClause.absent("foo")])
86
128
  end
87
129
  end
88
130
  describe "#before_days" do
89
131
  it "adds a date range query clause" do
90
132
  subject.before_days("foo", 60)
91
- expect(subject.clauses).to eq(["foo:[* TO NOW-60DAYS]"])
133
+ expect(subject.clauses).to eq([QueryClause.before_days("foo", 60)])
92
134
  end
93
135
  end
94
136
  describe "#before" do
95
137
  it "adds a date range query clause" do
96
138
  subject.before("foo", DateTime.parse("Thu, 27 Aug 2015 17:42:34 -0400"))
97
- expect(subject.clauses).to eq(["foo:[* TO 2015-08-27T21:42:34Z]"])
139
+ expect(subject.clauses).to eq([QueryClause.before("foo", DateTime.parse("Thu, 27 Aug 2015 17:42:34 -0400"))])
98
140
  end
99
141
  end
100
142
  end
@@ -6,90 +6,111 @@ module Ddr::Index
6
6
  subject { described_class.new { id "test:1" } }
7
7
  specify {
8
8
  expect(subject.query.rows).to eq 1
9
- expect(subject.query.q).to eq "{!term f=id}test:1"
9
+ expect(subject.query.q).to eq QueryClause.id("test:1")
10
10
  }
11
11
  end
12
-
13
12
  describe "q" do
14
13
  subject { described_class.new { q "foo:bar" } }
15
14
  specify { expect(subject.query.q).to eq "foo:bar" }
16
15
  end
17
-
18
16
  describe "asc" do
19
- subject { described_class.new { asc "foo" } }
20
- specify { expect(subject.query.sort).to eq [SortOrder.new(field: "foo", order: "asc")] }
17
+ subject { described_class.new { asc "foo", "bar" } }
18
+ specify {
19
+ expect(subject.query.sort).to eq [SortOrder.asc("foo"), SortOrder.asc("bar")]
20
+ }
21
21
  end
22
-
23
22
  describe "desc" do
24
- subject { described_class.new { desc "foo" } }
25
- specify { expect(subject.query.sort).to eq [SortOrder.new(field: "foo", order: "desc")] }
23
+ subject { described_class.new { desc "foo", "bar" } }
24
+ specify {
25
+ expect(subject.query.sort).to eq [SortOrder.desc("foo"), SortOrder.desc("bar")]
26
+ }
26
27
  end
27
-
28
28
  describe "filter" do
29
29
  subject { described_class.new { filter Filter.where("foo"=>"bar") } }
30
30
  specify { expect(subject.query.filters).to eq [Filter.where("foo"=>"bar")] }
31
31
  end
32
-
32
+ describe "filters" do
33
+ subject {
34
+ described_class.new { filters Filter.where("foo"=>"bar"), Filter.where("bing"=>"bang") }
35
+ }
36
+ specify {
37
+ expect(subject.query.filters).to eq([Filter.where("foo"=>"bar"),
38
+ Filter.where("bing"=>"bang")
39
+ ])
40
+ }
41
+ end
33
42
  describe "field" do
34
43
  subject { described_class.new { field "foo", "bar" } }
35
44
  specify { expect(subject.query.fields).to include("foo", "bar") }
36
45
  end
37
-
38
46
  describe "fields" do
39
47
  subject { described_class.new { fields "foo", "bar" } }
40
48
  specify { expect(subject.query.fields).to include("foo", "bar") }
41
49
  end
42
-
43
50
  describe "sort" do
44
51
  subject { described_class.new { sort "foo"=>"asc", "bar"=>"desc" } }
45
- specify { expect(subject.query.sort).to eq [SortOrder.new(field: "foo", order: "asc"), SortOrder.new(field: "bar", order: "desc")] }
52
+ specify {
53
+ expect(subject.query.sort).to eq([SortOrder.new(field: "foo", order: "asc"),
54
+ SortOrder.new(field: "bar", order: "desc")
55
+ ])
56
+ }
46
57
  end
47
-
48
58
  describe "order_by" do
49
59
  subject { described_class.new { order_by "foo"=>"asc", "bar"=>"desc" } }
50
60
  specify { expect(subject.query.sort).to eq [SortOrder.new(field: "foo", order: "asc"), SortOrder.new(field: "bar", order: "desc")] }
51
61
  end
52
-
53
62
  describe "limit" do
54
63
  subject { described_class.new { limit 5 } }
55
64
  specify { expect(subject.query.rows).to eq 5 }
56
65
  end
57
-
58
66
  describe "rows" do
59
67
  subject { described_class.new { rows 5 } }
60
68
  specify { expect(subject.query.rows).to eq 5 }
61
69
  end
62
-
63
70
  describe "raw" do
64
71
  subject { described_class.new { raw "foo:bar" } }
65
72
  specify { expect(subject.query.filters).to eq [Filter.raw("foo:bar")] }
66
73
  end
67
-
68
74
  describe "where" do
69
75
  subject { described_class.new { where "foo"=>"bar" } }
70
76
  specify { expect(subject.query.filters).to eq [Filter.where("foo"=>"bar")] }
71
77
  end
72
-
73
78
  describe "absent" do
74
79
  subject { described_class.new { absent "foo" } }
75
80
  specify { expect(subject.query.filters).to eq [Filter.absent("foo")] }
76
81
  end
77
-
78
82
  describe "present" do
79
83
  subject { described_class.new { present "foo" } }
80
84
  specify { expect(subject.query.filters).to eq [Filter.present("foo")] }
81
85
  end
82
-
83
86
  describe "before" do
84
- subject { described_class.new { before "foo", DateTime.parse("2015-12-14T20:40:06Z") } }
85
- specify { expect(subject.query.filters).to eq [Filter.before("foo", DateTime.parse("2015-12-14T20:40:06Z"))] }
87
+ subject {
88
+ described_class.new { before "foo", DateTime.parse("2015-12-14T20:40:06Z") }
89
+ }
90
+ specify {
91
+ expect(subject.query.filters).to eq [Filter.before("foo", DateTime.parse("2015-12-14T20:40:06Z"))]
92
+ }
86
93
  end
87
-
88
94
  describe "before_days" do
89
95
  subject { described_class.new { before_days "foo", 7 } }
90
96
  specify { expect(subject.query.filters).to eq [Filter.before_days("foo", 7)] }
91
97
  end
92
98
  end
93
99
 
100
+ describe "using static filters" do
101
+ describe "has_content" do
102
+ before { subject.has_content }
103
+ specify { expect(subject.query.filters).to eq [Filter.has_content] }
104
+ end
105
+ describe "is_governed_by" do
106
+ before { subject.is_governed_by "test:1" }
107
+ specify { expect(subject.query.filters).to eq [Filter.is_governed_by("test:1")] }
108
+ end
109
+ describe "is_governed_by" do
110
+ before { subject.is_member_of_collection "test:1" }
111
+ specify { expect(subject.query.filters).to eq [Filter.is_member_of_collection("test:1")] }
112
+ end
113
+ end
114
+
94
115
  end
95
116
  end