bento_search 0.0.1

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 (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