summon 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. data/.specification +58 -0
  2. data/History.txt +4 -0
  3. data/Manifest.txt +52 -0
  4. data/PostInstall.txt +4 -0
  5. data/README.rdoc +77 -0
  6. data/Rakefile +25 -0
  7. data/bin/summon +10 -0
  8. data/bin/summonh +19 -0
  9. data/ispec/integration_spec.rb +61 -0
  10. data/lib/summon.rb +41 -0
  11. data/lib/summon/cli.rb +136 -0
  12. data/lib/summon/log.rb +40 -0
  13. data/lib/summon/schema.rb +109 -0
  14. data/lib/summon/schema/availability.rb +14 -0
  15. data/lib/summon/schema/citation.rb +11 -0
  16. data/lib/summon/schema/date.rb +23 -0
  17. data/lib/summon/schema/document.rb +84 -0
  18. data/lib/summon/schema/error.rb +4 -0
  19. data/lib/summon/schema/facet.rb +51 -0
  20. data/lib/summon/schema/query.rb +96 -0
  21. data/lib/summon/schema/range.rb +52 -0
  22. data/lib/summon/schema/search.rb +37 -0
  23. data/lib/summon/schema/suggestion.rb +5 -0
  24. data/lib/summon/service.rb +44 -0
  25. data/lib/summon/transport.rb +13 -0
  26. data/lib/summon/transport/canned.json +2327 -0
  27. data/lib/summon/transport/canned.rb +9 -0
  28. data/lib/summon/transport/errors.rb +12 -0
  29. data/lib/summon/transport/headers.rb +55 -0
  30. data/lib/summon/transport/http.rb +114 -0
  31. data/lib/summon/transport/qstring.rb +49 -0
  32. data/script/console +10 -0
  33. data/script/destroy +14 -0
  34. data/script/generate +14 -0
  35. data/spec/spec.opts +1 -0
  36. data/spec/spec_helper.rb +19 -0
  37. data/spec/summon/log_spec.rb +28 -0
  38. data/spec/summon/schema/availability_spec.rb +31 -0
  39. data/spec/summon/schema/citation_spec.rb +34 -0
  40. data/spec/summon/schema/date_spec.rb +12 -0
  41. data/spec/summon/schema/document_spec.rb +235 -0
  42. data/spec/summon/schema/facet_spec.rb +115 -0
  43. data/spec/summon/schema/query_spec.rb +163 -0
  44. data/spec/summon/schema/range_spec.rb +45 -0
  45. data/spec/summon/schema/search_spec.rb +62 -0
  46. data/spec/summon/schema_spec.rb +143 -0
  47. data/spec/summon/service_spec.rb +18 -0
  48. data/spec/summon/transport/headers_spec.rb +47 -0
  49. data/spec/summon/transport/http_spec.rb +29 -0
  50. data/spec/summon/transport/qstring_spec.rb +24 -0
  51. data/spec/summon_spec.rb +48 -0
  52. data/summon.gemspec +41 -0
  53. metadata +145 -0
@@ -0,0 +1,115 @@
1
+ require File.dirname(__FILE__) + '/../../spec_helper'
2
+
3
+ describe Summon::Facet do
4
+
5
+ it "should map" do
6
+ facet = Summon::Facet.new(JSON.parse(EXAMPLE_FACET_JSON))
7
+ facet.remove_src
8
+ facet.counts.each {|f| f.remove_src }
9
+ facet.to_yaml.should == EXPECTED_FACET_YAML
10
+
11
+ first = facet.counts.first
12
+ first.apply_command.should == "addFacetValueFilter(ContentType_sfacet,Book,false)"
13
+ first.apply_negated_command.should == "addFacetValueFilter(ContentType_sfacet,Book,true)"
14
+ first.remove_command.should == "eatMyShorts()"
15
+ end
16
+
17
+ it "should now how to escape values" do
18
+ count = Summon::FacetCount.new(:value => "the quick, brown, fox")
19
+ count.value.should == "the quick, brown, fox"
20
+ count.escaped_value.should == 'the quick\, brown\, fox'
21
+
22
+ Summon::FacetCount.new(:value => ': everything (else) and $1 or {is} it\ ').escaped_value.should ==
23
+ '\: everything \(else\) and \$1 or \{is\} it\\ '
24
+ end
25
+
26
+ EXAMPLE_FACET_JSON = <<-JSON
27
+ {
28
+ "pageSize": 10,
29
+ "displayName": "ContentType",
30
+ "combineMode": "or",
31
+ "pageNumber": 1,
32
+ "listValuesCommand": "listFacetValues(ContentType_sfacet,or)",
33
+ "hasAppliedValue": false,
34
+ "fieldName": "ContentType_sfacet",
35
+ "hasLimitingValue": true,
36
+ "removeCommand": "removeFacetField(ContentType_sfacet)",
37
+ "removeValueFiltersCommand": "removeFacetValueFilters(ContentType)",
38
+ "counts": [
39
+ {
40
+ "isFurtherLimiting": true,
41
+ "isNegated": false,
42
+ "value": "Book",
43
+ "count": 799602,
44
+ "applyCommand": "addFacetValueFilter(ContentType_sfacet,Book,false)",
45
+ "removeCommand": "eatMyShorts()",
46
+ "applyNegatedCommand": "addFacetValueFilter(ContentType_sfacet,Book,true)",
47
+ "isApplied": false
48
+ },
49
+ {
50
+ "isFurtherLimiting": true,
51
+ "isNegated": false,
52
+ "value": "JournalArticle",
53
+ "count": 49765,
54
+ "applyCommand": "addFacetValueFilter(ContentType_sfacet,JournalArticle,false)",
55
+ "removeCommand": "eatMyShorts()",
56
+ "applyNegatedCommand": "addFacetValueFilter(ContentType_sfacet,JournalArticle,true)",
57
+ "isApplied": false
58
+ },
59
+ {
60
+ "isFurtherLimiting": true,
61
+ "isNegated": false,
62
+ "value": "Journal Article",
63
+ "count": 179002,
64
+ "removeCommand": "eatMyShorts()",
65
+ "applyCommand": "addFacetValueFilter(ContentType_sfacet,Journal Article,false)",
66
+ "applyNegatedCommand": "addFacetValueFilter(ContentType_sfacet,Journal Article,true)",
67
+ "isApplied": false
68
+ }
69
+ ]
70
+ }
71
+ JSON
72
+
73
+ EXPECTED_FACET_YAML = <<-YAML
74
+ --- !ruby/object:Summon::Facet
75
+ combine_mode: or
76
+ counts:
77
+ - !ruby/object:Summon::FacetCount
78
+ applied: false
79
+ apply_command: addFacetValueFilter(ContentType_sfacet,Book,false)
80
+ apply_negated_command: addFacetValueFilter(ContentType_sfacet,Book,true)
81
+ count: 799602
82
+ further_limiting: true
83
+ negated: false
84
+ remove_command: eatMyShorts()
85
+ src:
86
+ value: Book
87
+ - !ruby/object:Summon::FacetCount
88
+ applied: false
89
+ apply_command: addFacetValueFilter(ContentType_sfacet,JournalArticle,false)
90
+ apply_negated_command: addFacetValueFilter(ContentType_sfacet,JournalArticle,true)
91
+ count: 49765
92
+ further_limiting: true
93
+ negated: false
94
+ remove_command: eatMyShorts()
95
+ src:
96
+ value: JournalArticle
97
+ - !ruby/object:Summon::FacetCount
98
+ applied: false
99
+ apply_command: addFacetValueFilter(ContentType_sfacet,Journal Article,false)
100
+ apply_negated_command: addFacetValueFilter(ContentType_sfacet,Journal Article,true)
101
+ count: 179002
102
+ further_limiting: true
103
+ negated: false
104
+ remove_command: eatMyShorts()
105
+ src:
106
+ value: Journal Article
107
+ display_name: ContentType
108
+ field_name: ContentType_sfacet
109
+ page_number: 1
110
+ page_size: 10
111
+ remove_command: removeFacetField(ContentType_sfacet)
112
+ remove_value_filters_command: removeFacetValueFilters(ContentType)
113
+ src:
114
+ YAML
115
+ end
@@ -0,0 +1,163 @@
1
+ require File.dirname(__FILE__) + '/../../spec_helper'
2
+
3
+ describe Summon::Query do
4
+ it "maps" do
5
+ raw = JSON.parse(<<-JSON)
6
+ {
7
+ "searchTerms": [1],
8
+ "facetValueGroupFilters": [],
9
+ "queryString": "4",
10
+ "facetValueFilters": [],
11
+ "textQueries": [6],
12
+ "textFilters": [7],
13
+ "rangeFilters": [],
14
+ "facetFields": [8],
15
+ "rangeFacetFields": [9],
16
+ "sort": [
17
+ {
18
+ "fieldName": "PublicationDate_dt",
19
+ "sortOrder": "desc"
20
+ }
21
+ ],
22
+ "params": [11]
23
+ }
24
+ JSON
25
+
26
+
27
+ query(raw).with {
28
+ search_terms.should == [1]
29
+ range_filters.should == []
30
+ facet_value_group_filters.should == []
31
+ query_string.should == "4"
32
+ facet_value_filters.should == []
33
+ text_queries.should == [6]
34
+ text_filters.should == [7]
35
+ facet_fields.should == [8]
36
+ sorts.length.should == 1
37
+ sort.field_name.should == "PublicationDate_dt"
38
+ sort.sort_order.should == "desc"
39
+ }
40
+ end
41
+
42
+ it "should map facet value fields and facet group fields" do
43
+ q = query(
44
+ "searchTerms"=>[], "rangeFilters"=>[],
45
+ "facetValueGroupFilters"=>[
46
+ {
47
+ "combineMode"=>"or", "tag"=>"3",
48
+ "values"=>[
49
+ {"value"=>"history", "removeCommand"=>"removeFacetValueGroupFilter(3,history)"},
50
+ {"value"=>"uk", "removeCommand"=>"removeFacetValueGroupFilter(3,uk)"}
51
+ ],
52
+ "removeCommand"=>"removeFacetValueGroupFilter(3)",
53
+ "fieldName"=>"SubjectTerms"
54
+ }
55
+ ],
56
+ "facetValueFilters"=>[
57
+ {
58
+ "isNegated"=>false,
59
+ "value"=>"law",
60
+ "removeCommand"=>"removeFacetValueFilter(SubjectTerms,law)",
61
+ "fieldName"=>"SubjectTerms",
62
+ "negateCommand"=>"negateFacetValueFilter(SubjectTerms,law)"
63
+ }
64
+ ],
65
+ "queryString"=>"s.fvgf%3A3=SubjectTerms%2Cor%2Chistory%2Cuk&s.ff=SubjectTerms%2Cand%2C1%2C10&s.fvf=SubjectTerms%2Claw%2Cfalse",
66
+ "facetFields"=>[{
67
+ "pageSize"=>10, "pageNumber"=>1, "combineMode"=>"and", "removeCommand"=>"removeFacetField(SubjectTerms)", "fieldName"=>"SubjectTerms"
68
+ }],
69
+ "textFilters"=>[], "textQueries"=>[], "sort"=>[], "rangeFacetFields"=>[]
70
+ )
71
+ filter = q.facet_value_filters[0]
72
+ filter.should_not be_negated
73
+ filter.remove_command.should == "removeFacetValueFilter(SubjectTerms,law)"
74
+ filter.field_name.should == "SubjectTerms"
75
+ filter.negate_command.should == "negateFacetValueFilter(SubjectTerms,law)"
76
+
77
+ group = q.facet_value_group_filters[0]
78
+ group.combine_mode.should == "or"
79
+ group.tag.should == "3"
80
+ group.remove_command.should == "removeFacetValueGroupFilter(3)"
81
+ group.field_name.should == "SubjectTerms"
82
+
83
+ group.values[0].value.should == "history"
84
+ group.values[0].remove_command.should == "removeFacetValueGroupFilter(3,history)"
85
+ end
86
+
87
+ it "should be able to deescape a string" do
88
+ query_hash("").should == {}
89
+ query_hash("s.fq=Author_t%3A%28Linster%2C+Richard+L%29").should == {"s.fq" => "Author_t:(Linster, Richard L)"}
90
+ query_hash("s.fq=Balthazar&s.fq=Author_t%3A%28Linster%2C+Richard+L%29").should == {"s.fq" => ["Balthazar", "Author_t:(Linster, Richard L)"]}
91
+ query_hash("s.fq=Author_t%3A%28Linster%2C+Richard+L%29&s.q=Probability+models+of+recidivism%5C%3A+an+exploration").should ==
92
+ {"s.fq" => "Author_t:(Linster, Richard L)", "s.q" => "Probability models of recidivism\\: an exploration"}
93
+ end
94
+
95
+ it "has params" do
96
+ query({
97
+ :params => [
98
+ {
99
+ "key" => "holdingsOnly",
100
+ "value" => "t",
101
+ "removeCommand" => "removeParameter(holdingsOnly)"
102
+ }
103
+ ]
104
+ }).tap do |q|
105
+ q.params.should_not be(nil)
106
+ # what's the deal w/ holdings??? should we delete this test? - jl
107
+ # q.should be_holdings_only_enabled
108
+ end
109
+ end
110
+
111
+ it "knows it's own date_min and date_max" do
112
+ query({
113
+ :rangeFilters => [
114
+ {
115
+ "fieldName" => "PublicationDate",
116
+ "range" => {
117
+ "minValue" => "2000",
118
+ "maxValue" => "2008"
119
+ }
120
+ }
121
+ ]
122
+ }).tap do |q|
123
+ q.date_min.should == 2000
124
+ q.date_max.should == 2008
125
+ end
126
+ end
127
+
128
+ it "doesn't get phased when one of the ranges in unbounded" do
129
+ query({
130
+ :rangeFilters => [
131
+ {
132
+ "fieldName" => "PublicationDate",
133
+ "range" => {
134
+ "minValue" => "*",
135
+ "maxValue" => "2008"
136
+ }
137
+ }
138
+ ]
139
+ }).tap do |q|
140
+ q.date_min.should be_nil
141
+ q.date_max.should == 2008
142
+ end
143
+ end
144
+
145
+ it "should know about holdings only" do
146
+ query({}).should_not be_holdings_only_enabled
147
+ query(:isHoldingsOnlyEnabled => true).should be_holdings_only_enabled
148
+ query(:isHoldingsOnlyEnabled => false).should_not be_holdings_only_enabled
149
+ end
150
+
151
+ def query_hash(str)
152
+ query(:queryString => str).to_hash
153
+ end
154
+
155
+ def query(params)
156
+ Summon::Query.new(params)
157
+ end
158
+
159
+ end
160
+
161
+ class Object
162
+ alias_method :with, :instance_eval
163
+ end
@@ -0,0 +1,45 @@
1
+ require File.dirname(__FILE__) + '/../../spec_helper'
2
+
3
+ describe Summon::RangeFacet do
4
+ it "maps" do
5
+ range = Summon::RangeFacet.new(JSON.parse(<<-JSON))
6
+ {
7
+ "displayName": "PublicationDate",
8
+ "removeCommand": "removeFacetField(PublicationDate)",
9
+ "fieldName": "PublicationDate_dt",
10
+ "counts": [
11
+ {
12
+ "count": 119795,
13
+ "applyCommand": "addRangeFilter(PublicationDate,1999:1999)",
14
+ "range": {
15
+ "minValue": "1999",
16
+ "maxValue": "2000"
17
+ },
18
+ "isApplied": true
19
+ },
20
+ {
21
+ "count": 122712,
22
+ "applyCommand": "addRangeFilter(PublicationDate,2000:2000)",
23
+ "range": {
24
+ "minValue": "2000",
25
+ "maxValue": "2001"
26
+ },
27
+ "isApplied": false
28
+ }
29
+ ]
30
+ }
31
+ JSON
32
+ range.display_name.should == "PublicationDate"
33
+ range.remove_command.should == "removeFacetField(PublicationDate)"
34
+ range.field_name.should == "PublicationDate_dt"
35
+ range.counts.should_not be_nil
36
+ range.counts.length.should be(2)
37
+ range.counts[0].tap do |c|
38
+ c.count.should == 119795
39
+ c.apply_command.should == "addRangeFilter(PublicationDate,1999:1999)"
40
+ c.min.should == "1999"
41
+ c.max.should == "2000"
42
+ c.should be_applied
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,62 @@
1
+ require File.dirname(__FILE__) + '/../../spec_helper'
2
+
3
+ describe Summon::Search do
4
+ it "maps" do
5
+ search = Summon::Search.new(JSON.parse(<<-JSON))
6
+ {
7
+ "pageCount": 0,
8
+ "didYouMeanSuggestions": [
9
+
10
+ ],
11
+ "fullTextCount": 0,
12
+ "queryTime": 124,
13
+ "documents": [],
14
+ "totalRequestTime": 129,
15
+ "elapsedQueryTime": 126,
16
+ "version": "1.0.0",
17
+ "facetFields": [
18
+ ],
19
+ "rangeFacetFields": [
20
+
21
+ ],
22
+ "recordCount": 4589937,
23
+ "sessionId": "cfaa4020-1abe-4a9d-ae6e-e433a36c1069",
24
+ "query": {}
25
+ }
26
+ JSON
27
+ search.page_count.should == 0
28
+ search.record_count.should == 4589937
29
+ search.query_time.should == 124
30
+ search.session_id.should == "cfaa4020-1abe-4a9d-ae6e-e433a36c1069"
31
+ search.documents.should == []
32
+ search.version.should == "1.0.0"
33
+ search.query.should be_kind_of(Summon::Query)
34
+ end
35
+
36
+ it "should handle an error case" do
37
+ search = Summon::Search.new(
38
+ "version" => "1.0.0",
39
+ "errors" => [
40
+ {
41
+ "suggestion" => {
42
+ "applySuggestionCommand" => "removeTextQuery(foo\:bar) addTextQuery(foo\\\:bar)",
43
+ "suggestedQuery" => "foo\:bar",
44
+ "originalQuery" => "foo:bar"
45
+ },
46
+ "message" => "Unknown search field(s): foo."
47
+ }])
48
+ search.version.should == "1.0.0"
49
+ search.record_count.should == 0
50
+ search.errors.size.should == 1
51
+ search.errors.first.message.should == "Unknown search field(s): foo."
52
+ search.errors.first.suggestion.apply_suggestion_command.should == "removeTextQuery(foo\:bar) addTextQuery(foo\\\:bar)"
53
+ search.errors.first.suggestion.suggested_query.should == "foo\:bar"
54
+ search.errors.first.suggestion.original_query.should == "foo:bar"
55
+ end
56
+
57
+ it "should be empty w/ no docs" do
58
+ Summon::Search.new({}).should be_empty
59
+ Summon::Search.new({"documents" => [{}]}).should_not be_empty
60
+ end
61
+
62
+ end
@@ -0,0 +1,143 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe Summon::Schema do
4
+
5
+ before(:each) do
6
+ @class = Class.new(Summon::Schema)
7
+ end
8
+
9
+ def class_eval(&block)
10
+ @class.class_eval(&block)
11
+ end
12
+
13
+ def init(values)
14
+ @class.new(values)
15
+ end
16
+
17
+ it "pulls its attributes from a hash" do
18
+ class_eval do
19
+ attr :foo
20
+ attr :baz
21
+ end
22
+
23
+ init(:foo => "bar", :baz => "bang").foo.should == "bar"
24
+ end
25
+
26
+ it "can pull its attributes from string keys as well as symbol keys" do
27
+ class_eval do
28
+ attr :foo
29
+ end
30
+
31
+ init("foo" => "bar").foo.should == "bar"
32
+ end
33
+
34
+ it "knows how to atomagically map between camel case and perl case" do
35
+ class_eval do
36
+ attr :foo_bar_baz
37
+ end
38
+
39
+ init("fooBarBaz" => "Hello").foo_bar_baz.should == "Hello"
40
+ end
41
+
42
+ it "can also magically convert from Pascal case as well" do
43
+ class_eval do
44
+ attr :foo_bar_baz
45
+ end
46
+ init("FooBarBaz" => "Hello").foo_bar_baz.should == "Hello"
47
+ end
48
+
49
+ it "can have an overridden field name for names that don't map so cleanly" do
50
+ class_eval do
51
+ attr :isbn, :json_name => "ISBN"
52
+ attr :eissn, :json_name => "eISSN"
53
+ end
54
+
55
+ o = init("ISBN" => 123456, "eISSN" => 98765)
56
+ o.isbn.should == 123456
57
+ o.eissn.should == 98765
58
+ end
59
+
60
+ describe "transforms" do
61
+ it "can transform subobjects" do
62
+ class Summon::DogPile < Summon::Schema
63
+ attr :type
64
+ end
65
+ class_eval do
66
+ attr :steaming, :transform => :DogPile
67
+ end
68
+
69
+ o = init("steaming" => {:type => "swirled"})
70
+ o.steaming.should_not be_nil
71
+ o.steaming.type.should == "swirled"
72
+ end
73
+
74
+ it "will apply a transform across an array if the field is an array" do
75
+ class Summon::DogPatch < Summon::Schema
76
+ attr :foo
77
+ end
78
+ class_eval do
79
+ attr :patches, :transform => :DogPatch
80
+ end
81
+ patches = init("patches" => [{:foo => 'bar'},{:foo => 'baz'}, {:foo => 'bang'}]).patches
82
+ patches.should_not be_nil
83
+ patches.should be_kind_of(Array)
84
+ patches.collect{|p| p.foo}.should == ['bar', 'baz', 'bang']
85
+ end
86
+
87
+ it "will ignore a transform if the element is not present" do
88
+ class Summon::DogFood < Summon::Schema
89
+ attr :foo
90
+ end
91
+ class_eval do
92
+ attr :kibbles, :transform => :DogPatch
93
+ attr :bits, :single => true, :transform => :DogPatch
94
+ end
95
+ foo = init({})
96
+ foo.kibbles.should == []
97
+ foo.bits.should == nil
98
+ end
99
+ end
100
+
101
+ it "will create a predicate if the field is marked as boolean" do
102
+ class_eval do
103
+ attr :boollocks, :boolean => true
104
+ end
105
+
106
+ init("isBoollocks" => true).boollocks?.should be(true)
107
+ end
108
+
109
+ it "will automatically do a bean styles lookup on attributes that end in a question mark" do
110
+ class_eval do
111
+ attr :awesome?
112
+ end
113
+ init("isAwesome" => true).should be_awesome
114
+ init("isAwesome" => false).should_not be_awesome
115
+ end
116
+
117
+ it "automatically unwraps arrays from values if the value is marked as being single" do
118
+ class_eval do
119
+ attr :just_one, :single => true
120
+ end
121
+ init(:justOne => ["foo"]).just_one.should == 'foo'
122
+ end
123
+
124
+ it "defaults to automatically unwrap arrays if the field doesn't end in s" do
125
+ class_eval do
126
+ attr :foo
127
+ attr :foos
128
+ end
129
+ o = init(:foo => ["bar"], :foos => ["bar"])
130
+ o.foo.should == "bar"
131
+ o.foos.should == ["bar"]
132
+ end
133
+
134
+ it "initializes multi-valued fields to the empty array if it is not present" do
135
+ class_eval do
136
+ attr :foos
137
+ end
138
+ init({}).foos.should == []
139
+ end
140
+
141
+
142
+
143
+ end