bento_search 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (122) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.md +299 -0
  3. data/Rakefile +40 -0
  4. data/app/assets/images/bento_search/large_loader.gif +0 -0
  5. data/app/assets/javascripts/bento_search.js +3 -0
  6. data/app/assets/javascripts/bento_search/ajax_load.js +22 -0
  7. data/app/assets/stylesheets/bento_search/bento.css +4 -0
  8. data/app/controllers/bento_search/bento_search_controller.rb +7 -0
  9. data/app/controllers/bento_search/search_controller.rb +72 -0
  10. data/app/helpers/bento_search_helper.rb +138 -0
  11. data/app/item_decorators/bento_search/only_premade_openurl.rb +16 -0
  12. data/app/item_decorators/bento_search/openurl_add_other_link.rb +35 -0
  13. data/app/item_decorators/bento_search/openurl_main_link.rb +30 -0
  14. data/app/models/bento_search/author.rb +25 -0
  15. data/app/models/bento_search/link.rb +30 -0
  16. data/app/models/bento_search/multi_searcher.rb +109 -0
  17. data/app/models/bento_search/openurl_creator.rb +128 -0
  18. data/app/models/bento_search/registrar.rb +70 -0
  19. data/app/models/bento_search/result_item.rb +203 -0
  20. data/app/models/bento_search/results.rb +54 -0
  21. data/app/models/bento_search/results/pagination.rb +67 -0
  22. data/app/models/bento_search/search_engine.rb +219 -0
  23. data/app/models/bento_search/search_engine/capabilities.rb +65 -0
  24. data/app/search_engines/bento_search/#Untitled-1# +11 -0
  25. data/app/search_engines/bento_search/ebsco_host_engine.rb +356 -0
  26. data/app/search_engines/bento_search/eds_engine.rb +557 -0
  27. data/app/search_engines/bento_search/google_books_engine.rb +184 -0
  28. data/app/search_engines/bento_search/primo_engine.rb +231 -0
  29. data/app/search_engines/bento_search/scopus_engine.rb +295 -0
  30. data/app/search_engines/bento_search/summon_engine.rb +398 -0
  31. data/app/search_engines/bento_search/xerxes_engine.rb +168 -0
  32. data/app/views/bento_search/_link.html.erb +4 -0
  33. data/app/views/bento_search/_search_error.html.erb +22 -0
  34. data/app/views/bento_search/_std_item.html.erb +39 -0
  35. data/app/views/bento_search/search/search.html.erb +1 -0
  36. data/config/locales/en.yml +25 -0
  37. data/lib/bento_search.rb +29 -0
  38. data/lib/bento_search/engine.rb +5 -0
  39. data/lib/bento_search/routes.rb +45 -0
  40. data/lib/bento_search/version.rb +3 -0
  41. data/lib/generators/bento_search/pull_ebsco_dbs_generator.rb +24 -0
  42. data/lib/generators/bento_search/templates/ebsco_global_var.erb +6 -0
  43. data/lib/http_client_patch/include_client.rb +86 -0
  44. data/lib/tasks/bento_search_tasks.rake +4 -0
  45. data/test/dummy/README.rdoc +261 -0
  46. data/test/dummy/Rakefile +7 -0
  47. data/test/dummy/app/assets/javascripts/application.js +15 -0
  48. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  49. data/test/dummy/app/controllers/application_controller.rb +3 -0
  50. data/test/dummy/app/helpers/application_helper.rb +2 -0
  51. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  52. data/test/dummy/config.ru +4 -0
  53. data/test/dummy/config/application.rb +56 -0
  54. data/test/dummy/config/boot.rb +10 -0
  55. data/test/dummy/config/database.yml +25 -0
  56. data/test/dummy/config/environment.rb +5 -0
  57. data/test/dummy/config/environments/development.rb +37 -0
  58. data/test/dummy/config/environments/production.rb +67 -0
  59. data/test/dummy/config/environments/test.rb +37 -0
  60. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  61. data/test/dummy/config/initializers/inflections.rb +15 -0
  62. data/test/dummy/config/initializers/mime_types.rb +5 -0
  63. data/test/dummy/config/initializers/secret_token.rb +7 -0
  64. data/test/dummy/config/initializers/session_store.rb +8 -0
  65. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  66. data/test/dummy/config/locales/en.yml +5 -0
  67. data/test/dummy/config/routes.rb +6 -0
  68. data/test/dummy/db/test.sqlite3 +0 -0
  69. data/test/dummy/log/test.log +3100 -0
  70. data/test/dummy/public/404.html +26 -0
  71. data/test/dummy/public/422.html +26 -0
  72. data/test/dummy/public/500.html +25 -0
  73. data/test/dummy/public/favicon.ico +0 -0
  74. data/test/dummy/script/rails +6 -0
  75. data/test/functional/bento_search/search_controller_test.rb +81 -0
  76. data/test/helper/bento_search_helper_test.rb +125 -0
  77. data/test/integration/navigation_test.rb +10 -0
  78. data/test/support/mock_engine.rb +23 -0
  79. data/test/support/test_with_cassette.rb +38 -0
  80. data/test/test_helper.rb +52 -0
  81. data/test/unit/#vcr_test.rb# +68 -0
  82. data/test/unit/ebsco_host_engine_test.rb +134 -0
  83. data/test/unit/eds_engine_test.rb +105 -0
  84. data/test/unit/google_books_engine_test.rb +93 -0
  85. data/test/unit/item_decorators_test.rb +66 -0
  86. data/test/unit/multi_searcher_test.rb +49 -0
  87. data/test/unit/openurl_creator_test.rb +111 -0
  88. data/test/unit/pagination_test.rb +59 -0
  89. data/test/unit/primo_engine_test.rb +37 -0
  90. data/test/unit/register_engine_test.rb +50 -0
  91. data/test/unit/result_item_display_test.rb +39 -0
  92. data/test/unit/result_item_test.rb +36 -0
  93. data/test/unit/scopus_engine_test.rb +130 -0
  94. data/test/unit/search_engine_base_test.rb +178 -0
  95. data/test/unit/search_engine_test.rb +95 -0
  96. data/test/unit/summon_engine_test.rb +161 -0
  97. data/test/unit/xerxes_engine_test.rb +70 -0
  98. data/test/vcr_cassettes/ebscohost/error_bad_db.yml +45 -0
  99. data/test/vcr_cassettes/ebscohost/error_bad_password.yml +45 -0
  100. data/test/vcr_cassettes/ebscohost/get_info.yml +3626 -0
  101. data/test/vcr_cassettes/ebscohost/live_search.yml +45 -0
  102. data/test/vcr_cassettes/ebscohost/live_search_smoke_test.yml +1311 -0
  103. data/test/vcr_cassettes/eds/basic_search_smoke_test.yml +1811 -0
  104. data/test/vcr_cassettes/eds/get_auth_token.yml +75 -0
  105. data/test/vcr_cassettes/eds/get_auth_token_failure.yml +39 -0
  106. data/test/vcr_cassettes/eds/get_with_auth.yml +243 -0
  107. data/test/vcr_cassettes/eds/get_with_auth_recovers_from_bad_auth.yml +368 -0
  108. data/test/vcr_cassettes/gbs/error_condition.yml +40 -0
  109. data/test/vcr_cassettes/gbs/pagination.yml +702 -0
  110. data/test/vcr_cassettes/gbs/search.yml +340 -0
  111. data/test/vcr_cassettes/primo/search_smoke_test.yml +1112 -0
  112. data/test/vcr_cassettes/scopus/bad_api_key_should_return_error_response.yml +60 -0
  113. data/test/vcr_cassettes/scopus/escaped_chars.yml +187 -0
  114. data/test/vcr_cassettes/scopus/fielded_search.yml +176 -0
  115. data/test/vcr_cassettes/scopus/simple_search.yml +227 -0
  116. data/test/vcr_cassettes/scopus/zero_results_search.yml +67 -0
  117. data/test/vcr_cassettes/summon/bad_auth.yml +54 -0
  118. data/test/vcr_cassettes/summon/proper_tags_for_snippets.yml +216 -0
  119. data/test/vcr_cassettes/summon/search.yml +242 -0
  120. data/test/vcr_cassettes/xerxes/live_search.yml +2580 -0
  121. data/test/view/std_item_test.rb +98 -0
  122. metadata +421 -0
@@ -0,0 +1,70 @@
1
+
2
+ # Holds a list of registered search engines with configuration.
3
+ # There's one global one referened by BentoSearch module, but one
4
+ # might want to create multiple.
5
+ class BentoSearch::Registrar
6
+ class ::BentoSearch::NoSuchEngine < Exception ; end
7
+
8
+ def initialize
9
+ @registered_engine_confs = {}
10
+ end
11
+
12
+ # Register a configuration for a BentoSearch search engine.
13
+ # While some parts of BentoSearch can be used without globally registering
14
+ # a configuration, it is neccesary for features like AJAX load, and
15
+ # convenient in other places.
16
+ #
17
+ # BentoSearch.register_engine("gbs") do |conf|
18
+ # conf.engine = "GoogleBooksSearch"
19
+ # conf.api_key = "my_key"
20
+ # end
21
+ #
22
+ # BentoSearch.get_engine("gbs")
23
+ # => a BentoSearch::GoogleBooksSearch, configured as specified.
24
+ #
25
+ # The first parameter identifier, eg "gbs", may be used in some
26
+ # URLs, for AJAX etc.
27
+ def register_engine(id, &block)
28
+ conf = Confstruct::Configuration.new(&block)
29
+ conf.id = id.to_s
30
+
31
+ raise ArgumentError.new("Must supply an `engine` class name") unless conf.engine
32
+
33
+ @registered_engine_confs[id] = conf
34
+ end
35
+
36
+ # Get a configured SearchEngine, using configuration and engine
37
+ # class previously registered for `id` with #register_engine.
38
+ # Raises a BentoSearch::NoSuchEngine if is is not registered.
39
+ def get_engine(id)
40
+ conf = @registered_engine_confs[id.to_s]
41
+
42
+ raise BentoSearch::NoSuchEngine.new("No registered engine for identifier '#{id}'") unless conf
43
+
44
+ # Figure out which SearchEngine class to instantiate
45
+ klass = constantize(conf.engine)
46
+
47
+ return klass.new( conf )
48
+ end
49
+
50
+ # Mostly just used for testing
51
+ def reset_engine_registrations!
52
+ @@registered_engine_confs = {}
53
+ end
54
+
55
+ protected
56
+
57
+ # Turn a string into a constant/class object, lexical lookup
58
+ # within BentoSearch module. Can use whatever would be legal
59
+ # in ruby, "A", "A::B", "::A::B" (force top-level lookup), etc.
60
+ def constantize(klass_string)
61
+ unless /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/ =~ klass_string
62
+ raise NameError, "#{klass_string.inspect} is not a valid constant name!"
63
+ end
64
+
65
+ BentoSearch.module_eval(klass_string, __FILE__, __LINE__)
66
+ end
67
+
68
+
69
+
70
+ end
@@ -0,0 +1,203 @@
1
+ module BentoSearch
2
+ # Data object representing a single hit from a search, normalized
3
+ # with common data fields. Usually held in a BentoSearch::Results object.
4
+ #
5
+ # ANY field can be nil, clients should be aware.
6
+ class ResultItem
7
+ include ERB::Util # for html_escape for our presentational stuff
8
+ include ActionView::Helpers::OutputSafetyHelper # for safe_join
9
+
10
+ # Can initialize with a hash of key/values
11
+ def initialize(args = {})
12
+ args.each_pair do |key, value|
13
+ send("#{key}=", value)
14
+ end
15
+
16
+ self.authors ||= []
17
+ self.other_links ||= []
18
+
19
+ self.custom_data ||= {}
20
+ end
21
+
22
+ # Array (possibly empty) of BentoSearch::Link objects
23
+ # representing additional links. Often SearchEngine's themselves
24
+ # won't include any of these, but Decorators will be used
25
+ # to add them in.
26
+ attr_accessor :other_links
27
+
28
+ # * dc.title
29
+ # * schema.org CreativeWork: 'name'
30
+ attr_accessor :title
31
+
32
+ # When an individual seperate subtitle is available.
33
+ # May also be nil with subtitle in "title" field after colon.
34
+ #
35
+ # *
36
+ attr_accessor :subtitle
37
+
38
+ # usually a direct link to the search provider's 'native' page.
39
+ # Can be changed in actual presentation with a Decorator.
40
+ # * schema.org CreativeWork: 'url'
41
+ attr_accessor :link
42
+
43
+ # normalized controlled vocab title, important this is supplied
44
+ # if possible for OpenURL generation and other features.
45
+ #
46
+ # schema.org 'type' that's a sub-type of CreativeWork.
47
+ # should hold a string that, when appended to "http://schema.org/"
48
+ # is a valid schema.org type uri, that sub-types CreativeWork. Eg.
49
+ # * Article
50
+ # * Book
51
+ # * Movie
52
+ # * MusicRecording
53
+ # * Photograph
54
+ # * SoftwareApplication
55
+ # * WebPage
56
+ # * VideoObject
57
+ # * AudioObject
58
+ # * SoftwareApplication
59
+ #
60
+ #
61
+ #
62
+ # OR one of these symbols, sadly not covered by schema.org types:
63
+ # * :serial (magazine or journal)
64
+ # * :dissertation (dissertation or thesis)
65
+ # * :conference_paper # individual paper
66
+ # * :conference_proceedings # collected proceedings
67
+ # * :report # white paper or other report.
68
+ # * :book_item # section or exceprt from book.
69
+ #
70
+ # Note: We're re-thinking this, might allow uncontrolled
71
+ # in here instead.
72
+ attr_accessor :format
73
+
74
+ # uncontrolled presumably english-language format string.
75
+ # if supplied will be used in display in place of controlled
76
+ # format.
77
+ attr_accessor :format_str
78
+
79
+ # year published. a ruby int
80
+ # PART of:.
81
+ # * schema.org CreativeWork "datePublished", year portion
82
+ # * dcterms.issued, year portion
83
+ # * prism:coverDate, year portion
84
+ attr_accessor :year
85
+
86
+ attr_accessor :volume
87
+ attr_accessor :issue
88
+ attr_accessor :start_page
89
+ attr_accessor :end_page
90
+
91
+ attr_accessor :journal_title
92
+ attr_accessor :issn
93
+ attr_accessor :isbn
94
+
95
+ attr_accessor :doi
96
+
97
+ # usually used for books rather than articles
98
+ attr_accessor :publisher
99
+
100
+ # an openurl kev-encoded context object. optional,
101
+ # only if source provides one that may be better
102
+ # than can be constructed from individual elements above
103
+ attr_accessor :openurl_kev_co
104
+
105
+ # Short summary of item.
106
+ # Mark .html_safe if it includes html -- creator is responsible
107
+ # for making sure html is safely sanitizied and/or stripped,
108
+ # rails ActionView::Helpers::Sanistize #sanitize and #strip_tags
109
+ # may be helpful.
110
+ attr_accessor :abstract
111
+
112
+ # An array (order matters) of BentoSearch::Author objects
113
+ # add authors to it with results.authors << Author
114
+ attr_accessor :authors
115
+
116
+ # engine-specific data not suitable for abstract API, usually
117
+ # for internal use.
118
+ attr_accessor :custom_data
119
+
120
+
121
+ # Returns a ruby OpenURL::ContextObject (NISO Z39.88).
122
+ def to_openurl
123
+ BentoSearch::OpenurlCreator.new(self).to_openurl
124
+ end
125
+
126
+ ##################
127
+ # Presentation related methods.
128
+ # yes, it really makes sense to include them here, they can be overridden
129
+ # by decorators.
130
+ # May extract these to their own base decorator module at some point,
131
+ # but the OO hieararchy would be basically the same either way.
132
+ #######################
133
+
134
+
135
+ # How to display a BentoSearch::Author object as a name
136
+ def author_display(author)
137
+ if (author.first && author.last)
138
+ "#{author.last}, #{author.first.slice(0,1)}"
139
+ elsif author.display
140
+ author.display
141
+ elsif author.last
142
+ author.last
143
+ else
144
+ nil
145
+ end
146
+ end
147
+
148
+ # Put together title and subtitle if neccesary.
149
+ def complete_title
150
+ t = self.title
151
+ if self.subtitle
152
+ t = safe_join([t, ": ", self.subtitle], "")
153
+ end
154
+
155
+ if t.blank?
156
+ t = I18n.translate("bento_search.missing_title")
157
+ end
158
+
159
+ return t
160
+ end
161
+
162
+
163
+
164
+ # A simple user-displayable citation, _without_ author/title.
165
+ # the journal, year, vol, iss, page; or publisher and year; etc.
166
+ # Constructed from individual details. Not formal APA or MLA or anything,
167
+ # just a rough and ready display.
168
+ #
169
+ # TODO: Should this be moved to a rails helper method? Not sure.
170
+ def published_in
171
+ result_elements = []
172
+
173
+ unless year.blank?
174
+ # wrap year in a span so we can bold it.
175
+ result_elements.push "<span class='year'>#{year}</span>"
176
+ end
177
+
178
+ result_elements.push(journal_title) unless journal_title.blank?
179
+
180
+ if journal_title.blank? && ! publisher.blank?
181
+ result_elements.push html_escape publisher
182
+ end
183
+
184
+ if (! volume.blank?) && (! issue.blank?)
185
+ result_elements.push html_escape "#{volume}(#{issue})"
186
+ else
187
+ result_elements.push html_escape volume unless volume.blank?
188
+ result_elements.push html_escape issue unless issue.blank?
189
+ end
190
+
191
+ if (! start_page.blank?) && (! end_page.blank?)
192
+ result_elements.push html_escape "pp. #{start_page}-#{end_page}"
193
+ elsif ! start_page.blank?
194
+ result_elements.push html_escape "p. #{start_page}"
195
+ end
196
+
197
+ return nil if result_elements.empty?
198
+
199
+ return result_elements.join(", ").html_safe
200
+ end
201
+
202
+ end
203
+ end
@@ -0,0 +1,54 @@
1
+ module BentoSearch
2
+ # An array-like object (in fact it-subclasses Array) that holds
3
+ # a page of search results. But also has some meta-data about the
4
+ # search itself (query, paging, etc).
5
+
6
+ # If #error is non-nil, object may not have real results, but
7
+ # be an error. You can use failed? to see.
8
+ class Results < ::Array
9
+ attr_accessor :total_items
10
+ # 0-based index into total results, used for pagination
11
+ attr_accessor :start
12
+ # per_page setting, can be used for pagination.
13
+ attr_accessor :per_page
14
+
15
+ # If error is non-nil, it's an error condition with no real results.
16
+ # error should be a hash with these (and possibly other) keys, although
17
+ # none of these are required to be non-nil.
18
+ # [:status]
19
+ # A (usually) non-succesful HTTP status code. May be nil.
20
+ # [:message]
21
+ # A short message explaining error, usually provided by external
22
+ # service. NOT suitable for showing to end-users. May be nil.
23
+ # [:end_user_message]
24
+ # A message suitable for showing to end-users. May be nil.
25
+ # [:error_info]
26
+ # A service-specific way of reporting more error info, for developers,
27
+ # not suitable for end-users. Might be a string, might be a hash,
28
+ # depends on the service. may be nil.
29
+ # [:exception]
30
+ # Possibly a ruby exception object. may be nil.
31
+ attr_accessor :error
32
+
33
+ # time it took to do search, in seconds.
34
+ attr_accessor :timing
35
+
36
+ # search arguments as normalized by SearchEngine, not neccesarily
37
+ # directly as input. A hash.
38
+ attr_accessor :search_args
39
+ # Registered id of engine used to create these results,
40
+ # may be nil if used with an unregistered engine.
41
+ attr_accessor :engine_id
42
+
43
+ # Returns a BentoSearch::Results::Pagination, that should be suitable
44
+ # for passing right to Kaminari (although Kaminari isn't good about doc/specing
45
+ # it's api, so might break), or convenient methods for your own custom UI.
46
+ def pagination
47
+ Pagination.new( total_items, search_args)
48
+ end
49
+
50
+ def failed?
51
+ ! error.nil?
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,67 @@
1
+
2
+ # An object intended to be compatible with kaminari for pagination,
3
+ # although kaminari doesn't doc/spec exactly what you need, so might
4
+ # break in future. Could be useful for your own custom pagination too.
5
+ #
6
+ # You don't normally create one of these yourself, you get one returned
7
+ # from Results#pagination
8
+ class BentoSearch::Results::Pagination
9
+
10
+ # first arg is results.total_items, second is result
11
+ # of normalized_args from searchresults.
12
+ #
13
+ # We don't do the page/start normalization calc here,
14
+ # we count on them both being passed in, already calculated
15
+ # by normalize_arguments in SearchResults. Expect :page, 0-based
16
+ # :start, and :per_page
17
+ def initialize(total, normalized_args)
18
+ @total_count = total || 0
19
+ @per_page = normalized_args[:per_page] || 10
20
+ @current_page = normalized_args[:page] || 1
21
+ @start_record = (normalized_args[:start] || 0) + 1
22
+ end
23
+
24
+ def current_page
25
+ @current_page
26
+ end
27
+
28
+ # 1-based start, suitable for showing to user
29
+ # Can be 0 for empty result set.
30
+ def start_record
31
+ [@start_record, count_records].min
32
+ end
33
+
34
+ # 1-based last record in window, suitable for showing to user.
35
+ # Can be 0 for empty result set.
36
+ def end_record
37
+ [start_record + per_page - 1, count_records].min
38
+ end
39
+
40
+ # If nil is passed in, will normalize to 0 so things doing math
41
+ # comparisons won't raise.
42
+ def count_records
43
+ @total_count
44
+ end
45
+
46
+ def total_pages
47
+ (@total_count.to_f / @per_page).ceil
48
+ end
49
+ # kaminari wants both, weird.
50
+ alias num_pages total_pages
51
+
52
+
53
+ def first_page?
54
+ current_page == 1
55
+ end
56
+
57
+ def last_page?
58
+ current_page >= total_pages
59
+ end
60
+
61
+ def per_page
62
+ @per_page
63
+ end
64
+ # kaminari wants it called this.
65
+ alias limit_value per_page
66
+
67
+ end
@@ -0,0 +1,219 @@
1
+ require 'active_support/concern'
2
+ require 'active_support/core_ext/module/delegation'
3
+ require 'confstruct'
4
+
5
+
6
+ module BentoSearch
7
+ # Module mix-in for bento_search search engines.
8
+ #
9
+ # ==Using a SearchEngine
10
+ #
11
+ # * init/config
12
+ # * search
13
+ # * pagination, with max per_page
14
+ # * search fields, with semantics. ask for supported search fields.
15
+ #
16
+ # == Standard config
17
+ # * item_decorators : Array of Modules that will be decorated. See Decorators section.
18
+ #
19
+ # == Implementing a SearchEngine
20
+ #
21
+ # `include BentoSearch::SearchEngine`
22
+ #
23
+ # a SearchEngine's state should not be search-specific, but
24
+ # is configuration specific. Don't store anything specific
25
+ # to a specific search in iVars.
26
+ #
27
+ # Do implement `#search(*args)`
28
+ #
29
+ # Do use HTTPClient, if possible, for http searches,
30
+ # using a class-level HTTPClient to maintain persistent connections.
31
+ #
32
+ # Other options:
33
+ # * implement a class-level `self.required_configuration' returning
34
+ # an array of config keys or dot keypaths, and it'll raise on init
35
+ # if those config's weren't supplied.
36
+ # * max per page
37
+ # * search fields
38
+ #
39
+ # Some engines support `:auth => true` for elevated access to affiliated
40
+ # users.
41
+ #
42
+ module SearchEngine
43
+ DefaultPerPage = 10
44
+
45
+ extend ActiveSupport::Concern
46
+
47
+ include Capabilities
48
+
49
+ included do
50
+ attr_accessor :configuration
51
+ end
52
+
53
+ # If specific SearchEngine calls initialize, you want to call super
54
+ # handles configuration loading, mostly. Argument is a
55
+ # Confstruct::Configuration or Hash.
56
+ def initialize(aConfiguration = Confstruct::Configuration.new)
57
+ # init, from copy of default, or new
58
+ if self.class.default_configuration
59
+ self.configuration = Confstruct::Configuration.new(self.class.default_configuration)
60
+ else
61
+ self.configuration = Confstruct::Configuration.new
62
+ end
63
+ # merge in current instance config
64
+ self.configuration.configure ( aConfiguration )
65
+
66
+ # global defaults?
67
+ self.configuration[:item_decorators] ||= []
68
+
69
+ # check for required keys
70
+ if self.class.required_configuration
71
+ self.class.required_configuration.each do |required_key|
72
+ if self.configuration.lookup!(required_key.to_s, "**NOT_FOUND**") == "**NOT_FOUND**"
73
+ raise ArgumentError.new("#{self.class.name} requires configuration key #{required_key}")
74
+ end
75
+ end
76
+ end
77
+
78
+ end
79
+
80
+ # Calls individual engine #search_implementation.
81
+ # first normalizes arguments, also adds on standard metadata
82
+ # to results.
83
+ def search(*arguments)
84
+ start_t = Time.now
85
+
86
+ arguments = normalized_search_arguments(*arguments)
87
+
88
+ results = search_implementation(arguments)
89
+
90
+ decorate(results)
91
+
92
+ # standard result metadata
93
+ results.start = arguments[:start] || 0
94
+ results.per_page = arguments[:per_page]
95
+
96
+ results.search_args = arguments
97
+ results.engine_id = configuration.id
98
+
99
+ results.timing = (Time.now - start_t)
100
+
101
+ return results
102
+ end
103
+
104
+
105
+
106
+ def normalized_search_arguments(*orig_arguments)
107
+ arguments = {}
108
+
109
+ # Two-arg style to one hash, if present
110
+ if (orig_arguments.length > 1 ||
111
+ (orig_arguments.length == 1 && ! orig_arguments.first.kind_of?(Hash)))
112
+ arguments[:query] = orig_arguments.delete_at(0)
113
+ end
114
+
115
+ arguments.merge!(orig_arguments.first) if orig_arguments.length > 0
116
+
117
+
118
+ # allow strings for pagination (like from url query), change to
119
+ # int please.
120
+ [:page, :per_page, :start].each do |key|
121
+ arguments.delete(key) if arguments[key].blank?
122
+ arguments[key] = arguments[key].to_i if arguments[key]
123
+ end
124
+ arguments[:per_page] ||= DefaultPerPage
125
+
126
+ # illegal arguments
127
+ if (arguments[:start] && arguments[:page])
128
+ raise ArgumentError.new("Can't supply both :page and :start")
129
+ end
130
+ if ( arguments[:per_page] &&
131
+ self.max_per_page &&
132
+ arguments[:per_page] > self.max_per_page)
133
+ raise ArgumentError.new("#{arguments[:per_page]} is more than maximum :per_page of #{self.max_per_page} for #{self.class}")
134
+ end
135
+
136
+
137
+ # Normalize :page to :start, and vice versa
138
+ if arguments[:page]
139
+ arguments[:start] = (arguments[:page] - 1) * arguments[:per_page]
140
+ elsif arguments[:start]
141
+ arguments[:page] = (arguments[:start] / arguments[:per_page]) + 1
142
+ end
143
+
144
+ # normalize :sort from possibly symbol to string
145
+ # TODO: raise if unrecognized sort key?
146
+ if arguments[:sort]
147
+ arguments[:sort] = arguments[:sort].to_s
148
+ end
149
+
150
+ # translate semantic_search_field to search_field, or raise if
151
+ # can't.
152
+ if (semantic = arguments.delete(:semantic_search_field)) && ! semantic.blank?
153
+
154
+ mapped = self.semantic_search_map[semantic.to_s]
155
+ unless mapped
156
+ raise ArgumentError.new("#{self.class.name} does not know about :semantic_search_field #{semantic}")
157
+ end
158
+ arguments[:search_field] = mapped
159
+ end
160
+
161
+ return arguments
162
+ end
163
+ alias_method :parse_search_arguments, :normalized_search_arguments
164
+
165
+
166
+
167
+
168
+
169
+ protected
170
+
171
+ # Extend each result with each specified decorator module
172
+ def decorate(results)
173
+ results.each do |result|
174
+ configuration.item_decorators.each do |decorator|
175
+ result.extend decorator
176
+ end
177
+ end
178
+ end
179
+
180
+
181
+ module ClassMethods
182
+
183
+ # If support fielded search, over-ride to specify fields
184
+ # supported. Returns a hash, key is engine-specific internal
185
+ # search field, value is nil or a hash of metadata about
186
+ # the search field, including semantic mapping.
187
+ #
188
+ # def search_field_definitions
189
+ # { "intitle" => {:semantic => :title}}
190
+ # end
191
+ def search_field_definitions
192
+ {}
193
+ end
194
+
195
+
196
+ # Returns list of string internal search_field's that can
197
+ # be supplied to search(:search_field => x)
198
+ def search_keys
199
+ return search_field_definitions.keys
200
+ end
201
+
202
+
203
+
204
+ # Over-ride returning a hash or Confstruct with
205
+ # any configuration values you want by default.
206
+ # actual user-specified config values will be deep-merged
207
+ # into the defaults.
208
+ def default_configuration
209
+ end
210
+
211
+ # Over-ride returning an array of symbols for required
212
+ # configuration keys.
213
+ def required_configuration
214
+ end
215
+
216
+ end
217
+
218
+ end
219
+ end