celsius-primo 0.1.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 (55) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +8 -0
  5. data/Gemfile +15 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +51 -0
  8. data/Rakefile +6 -0
  9. data/celsius-primo.gemspec +29 -0
  10. data/lib/celsius/primo/adapter/mget.rb +36 -0
  11. data/lib/celsius/primo/adapter/operation.rb +17 -0
  12. data/lib/celsius/primo/adapter/search.rb +28 -0
  13. data/lib/celsius/primo/adapter.rb +52 -0
  14. data/lib/celsius/primo/locales/de.yml +455 -0
  15. data/lib/celsius/primo/locales/en.yml +207 -0
  16. data/lib/celsius/primo/soap_api/searcher/search_brief/search_request_transformation/add_sort_by_list.rb +36 -0
  17. data/lib/celsius/primo/soap_api/searcher/search_brief/search_request_transformation/embed_inner_search_request.rb +12 -0
  18. data/lib/celsius/primo/soap_api/searcher/search_brief/search_request_transformation/process_ids_queries.rb +29 -0
  19. data/lib/celsius/primo/soap_api/searcher/search_brief/search_request_transformation/process_match_queries.rb +29 -0
  20. data/lib/celsius/primo/soap_api/searcher/search_brief/search_request_transformation/process_query_string_queries.rb +37 -0
  21. data/lib/celsius/primo/soap_api/searcher/search_brief/search_request_transformation/process_range_queries.rb +28 -0
  22. data/lib/celsius/primo/soap_api/searcher/search_brief/search_request_transformation/serialize_target_as_xml.rb +10 -0
  23. data/lib/celsius/primo/soap_api/searcher/search_brief/search_request_transformation/set_bulk_size.rb +12 -0
  24. data/lib/celsius/primo/soap_api/searcher/search_brief/search_request_transformation/set_institution.rb +12 -0
  25. data/lib/celsius/primo/soap_api/searcher/search_brief/search_request_transformation/set_languages.rb +14 -0
  26. data/lib/celsius/primo/soap_api/searcher/search_brief/search_request_transformation/set_locations.rb +17 -0
  27. data/lib/celsius/primo/soap_api/searcher/search_brief/search_request_transformation/set_start_index.rb +12 -0
  28. data/lib/celsius/primo/soap_api/searcher/search_brief/search_request_transformation/setup_inner_search_request.rb +32 -0
  29. data/lib/celsius/primo/soap_api/searcher/search_brief/search_request_transformation/setup_target.rb +25 -0
  30. data/lib/celsius/primo/soap_api/searcher/search_brief/search_request_transformation.rb +59 -0
  31. data/lib/celsius/primo/soap_api/searcher/search_brief/search_result_transformation/add_missing_facets.rb +45 -0
  32. data/lib/celsius/primo/soap_api/searcher/search_brief/search_result_transformation/parse_inner_search_brief_return.rb +23 -0
  33. data/lib/celsius/primo/soap_api/searcher/search_brief/search_result_transformation/process_facets.rb +29 -0
  34. data/lib/celsius/primo/soap_api/searcher/search_brief/search_result_transformation/process_records.rb +80 -0
  35. data/lib/celsius/primo/soap_api/searcher/search_brief/search_result_transformation/remove_bogus_creationdate_facets.rb +15 -0
  36. data/lib/celsius/primo/soap_api/searcher/search_brief/search_result_transformation/select_only_requested_facets.rb +14 -0
  37. data/lib/celsius/primo/soap_api/searcher/search_brief/search_result_transformation/set_total_hits.rb +12 -0
  38. data/lib/celsius/primo/soap_api/searcher/search_brief/search_result_transformation/setup_target_skeleton.rb +17 -0
  39. data/lib/celsius/primo/soap_api/searcher/search_brief/search_result_transformation/sort_facets_by_search_request.rb +21 -0
  40. data/lib/celsius/primo/soap_api/searcher/search_brief/search_result_transformation.rb +59 -0
  41. data/lib/celsius/primo/soap_api/searcher/search_brief.rb +56 -0
  42. data/lib/celsius/primo/soap_api/searcher.rb +14 -0
  43. data/lib/celsius/primo/soap_api.rb +22 -0
  44. data/lib/celsius/primo/version.rb +5 -0
  45. data/lib/celsius/primo.rb +7 -0
  46. data/spec/assets/adapter/mget/mget_request.yml +4 -0
  47. data/spec/assets/adapter/mget/mget_result.yml +407 -0
  48. data/spec/assets/adapter/search/search_request.yml +59 -0
  49. data/spec/assets/adapter/search/search_result.yml +1517 -0
  50. data/spec/cassettes/Celsius_Primo_Adapter/_mget/returns_a_normalzed_mget_result.yml +396 -0
  51. data/spec/cassettes/Celsius_Primo_Adapter/_search/returns_a_normalized_search_result.yml +1169 -0
  52. data/spec/celsius-primo/adapter_spec.rb +56 -0
  53. data/spec/celsius-primo_spec.rb +6 -0
  54. data/spec/spec_helper.rb +55 -0
  55. metadata +255 -0
@@ -0,0 +1,12 @@
1
+ require "celsius/transformation/step"
2
+ require "ox"
3
+
4
+ class Celsius::Primo::SoapApi::Searcher::SearchBrief::SearchRequestTransformation::
5
+ SetStartIndex < Celsius::Transformation::Step
6
+
7
+ def call
8
+ transformation.inner_search_request.locate("PrimoSearchRequest/StartIndex").first.tap do |node|
9
+ node << (Celsius::Hash.deep_find_key(source, :from).first.to_i + 1).to_s
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,32 @@
1
+ require "celsius/transformation/step"
2
+ require "ox"
3
+
4
+ class Celsius::Primo::SoapApi::Searcher::SearchBrief::SearchRequestTransformation::
5
+ SetupInnerSearchRequest < Celsius::Transformation::Step
6
+
7
+ #
8
+ # setup inner search request that will be wrapped in a cdata element at the end
9
+ #
10
+ def call
11
+ # we setup this skeleton instead of dynamic element creation because order matters with primo
12
+ transformation.inner_search_request = Ox.parse(
13
+ <<-xml
14
+ <searchRequest xmlns="http://www.exlibris.com/primo/xsd/wsRequest" xmlns:uic="http://www.exlibris.com/primo/xsd/primoview/uicomponents">
15
+ <PrimoSearchRequest xmlns="http://www.exlibris.com/primo/xsd/search/request">
16
+ <QueryTerms>
17
+ <BoolOpeator></BoolOpeator>
18
+ </QueryTerms>
19
+ <StartIndex></StartIndex>
20
+ <BulkSize></BulkSize>
21
+ <DidUMeanEnabled>false</DidUMeanEnabled>
22
+ <HighlightingEnabled>false</HighlightingEnabled>
23
+ <Languages></Languages>
24
+ <Locations></Locations>
25
+ </PrimoSearchRequest>
26
+ <onCampus>false</onCampus>
27
+ <institution></institution>
28
+ </searchRequest>
29
+ xml
30
+ )
31
+ end
32
+ end
@@ -0,0 +1,25 @@
1
+ require "celsius/transformation/step"
2
+ require "ox"
3
+
4
+ class Celsius::Primo::SoapApi::Searcher::SearchBrief::SearchRequestTransformation::SetupTarget < Celsius::Transformation::Step
5
+ def call
6
+ # create empty ox document
7
+ self.target = Ox::Document.new(version: "1.0", encoding: "UTF-8")
8
+
9
+ # populate target with soap request skeleton
10
+ self.target << Ox.parse(
11
+ <<-xml
12
+ <env:Envelope
13
+ xmlns:xsd="http://www.w3.org/2001/XMLSchema"
14
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
15
+ xmlns:impl="http://primo.kobv.de/PrimoWebServices/services/searcher"
16
+ xmlns:env="http://schemas.xmlsoap.org/soap/envelope/"
17
+ xmlns:ins0="http://xml.apache.org/xml-soap">
18
+ <env:Body>
19
+ <impl:searchBrief><searchRequestStr></searchRequestStr></impl:searchBrief>
20
+ </env:Body>
21
+ </env:Envelope>
22
+ xml
23
+ )
24
+ end
25
+ end
@@ -0,0 +1,59 @@
1
+ require "celsius/primo/soap_api/searcher/search_brief"
2
+ require "celsius/transformation"
3
+
4
+ class Celsius::Primo::SoapApi::Searcher::SearchBrief::SearchRequestTransformation < Celsius::Transformation
5
+ require_directory "#{File.dirname(__FILE__)}/search_request_transformation"
6
+
7
+ sequence [
8
+ SetupTarget,
9
+ SetupInnerSearchRequest,
10
+ [
11
+ [
12
+ ProcessMatchQueries,
13
+ ProcessQueryStringQueries,
14
+ ProcessRangeQueries,
15
+ ProcessIdsQueries,
16
+ SetStartIndex,
17
+ SetBulkSize,
18
+ SetLanguages,
19
+ AddSortByList,
20
+ SetLocations
21
+ ],
22
+ SetInstitution
23
+ ],
24
+ EmbedInnerSearchRequest,
25
+ SerializeTargetAsXml
26
+ ]
27
+
28
+ attr_accessor :languages
29
+ attr_accessor :locations
30
+ attr_accessor :institution
31
+ attr_accessor :inner_search_request
32
+
33
+ def initialize(options = {})
34
+ @languages = options[:languages]
35
+ @locations = options[:locations]
36
+ @institution = options[:institution]
37
+ end
38
+
39
+ def index_field_mapping(index_field)
40
+ case index_field
41
+ when "_all" then "any"
42
+ when "created" then "cdate"
43
+ when "identifier" then "any"
44
+ when "language" then "lang"
45
+ when "subject" then "sub"
46
+ else index_field
47
+ end
48
+ end
49
+
50
+ def sort_field_mapping(sort_field)
51
+ case sort_field
52
+ when "_score" then "relevance"
53
+ when "created" then "scdate"
54
+ when "creator" then "screator"
55
+ when "title" then "stitle"
56
+ else sort_field
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,45 @@
1
+ require "celsius/transformation/step"
2
+ require "ox"
3
+
4
+ class Celsius::Primo::SoapApi::Searcher::SearchBrief::SearchResultTransformation::
5
+ AddMissingFacets < Celsius::Transformation::Step
6
+
7
+ #
8
+ # Sometimes, requested facets do not show up in search result, so we have to (re)add them
9
+ #
10
+ def call
11
+ if match_queries = Celsius::Hash.deep_find_key(search_request, :match)
12
+ binding.pry
13
+ raise "unimplemented!"
14
+ end
15
+ end
16
+
17
+ #
18
+ private
19
+ #
20
+ =begin
21
+ # handle primo bug where requested facets don't show up in search response
22
+ process "//match", source: -> { @search_request} do |match, target|
23
+ if (target_facet_name = match.nodes.first.value).start_with?("facet_")
24
+ target_facet_value = match.nodes.first.text
25
+ target_facet_count = find_all(@search_brief_return, "SEGMENTS/JAGROOT/RESULT/DOCSET/DOC").length
26
+
27
+ # case 1 - there is no facet at all
28
+ unless find(target, "facets/#{target_facet_name}")
29
+ find(target, "facets") << element(target_facet_name) do |target_facet|
30
+ target_facet << element("_type", text: "terms")
31
+ target_facet << array("terms")
32
+ end
33
+ end
34
+
35
+ # case 2 - there is a facet, but the requested facet is not included
36
+ unless find_all(target, "facets/#{target_facet_name}/terms/terms/term").map(&:text).include?(target_facet_value)
37
+ array(find(target, "facets/#{target_facet_name}/terms")) do |term|
38
+ term << element("term", text: target_facet_value)
39
+ term << element("count", text: target_facet_count.to_s, type: "integer")
40
+ end
41
+ end
42
+ end
43
+ end
44
+ =end
45
+ end
@@ -0,0 +1,23 @@
1
+ require "celsius/transformation/step"
2
+ require "ox"
3
+
4
+ class Celsius::Primo::SoapApi::Searcher::SearchBrief::SearchResultTransformation::
5
+ ParseInnerSearchBriefReturn < Celsius::Transformation::Step
6
+
7
+ def call
8
+ remove_namespaces!(source)
9
+
10
+ string_encoded_search_brief_return =
11
+ Ox.parse(source).locate("Envelope/Body/searchBriefResponse/searchBriefReturn").first
12
+
13
+ transformation.search_brief_return =
14
+ Ox.parse remove_namespaces!(string_encoded_search_brief_return.text)
15
+ end
16
+
17
+ #
18
+ private
19
+ #
20
+ def remove_namespaces!(xml)
21
+ xml.gsub!(/<(\/?)\w+:(\w+)/, "<\\1\\2")
22
+ end
23
+ end
@@ -0,0 +1,29 @@
1
+ require "celsius/transformation/step"
2
+ require "ox"
3
+
4
+ class Celsius::Primo::SoapApi::Searcher::SearchBrief::SearchResultTransformation::
5
+ ProcessFacets < Celsius::Transformation::Step
6
+
7
+ def call
8
+ if source_facets = search_brief_return.locate("JAGROOT/RESULT/FACETLIST/FACET")
9
+ source_facets.each do |source_facet|
10
+ target["facets"]["facet_#{source_facet["NAME"]}"] = target_facet(source_facet)
11
+ end
12
+ end
13
+ end
14
+
15
+ #
16
+ private
17
+ #
18
+ def target_facet(source_facet)
19
+ {
20
+ "_type" => "terms",
21
+ "terms" => source_facet.locate("FACET_VALUES").map do |source_facet_value|
22
+ {
23
+ "term" => source_facet_value["KEY"].force_encoding("utf-8"),
24
+ "count" => source_facet_value["VALUE"].to_i
25
+ }
26
+ end
27
+ }
28
+ end
29
+ end
@@ -0,0 +1,80 @@
1
+ require "celsius/transformation/step"
2
+ require "ox"
3
+
4
+ class Celsius::Primo::SoapApi::Searcher::SearchBrief::SearchResultTransformation::
5
+ ProcessRecords < Celsius::Transformation::Step
6
+
7
+ def call
8
+ add_records!(transformation.target).each do |record|
9
+ [
10
+ { "_source/control/recordid" => "_id" },
11
+ { "_source/display/creator" => "creator" },
12
+ { "_source/display/creationdate" => "created" },
13
+ { "_source/display/description" => "description" },
14
+ { "_source/display/edition" => "edition" },
15
+ { "_source/display/format" => "format" },
16
+ { "_source/control/ilsapiid" => "identifier" },
17
+ { "_source/control/recordid" => "identifier" },
18
+ { "_source/search/isbn" => "isbn" },
19
+ { "_source/search/issn" => "issn" },
20
+ { "_source/display/language" => "language" },
21
+ { "_source/display/title" => "title" },
22
+ { "_source/display/publisher" => "publisher" },
23
+ { "_source/display/subject" => "subject" }
24
+ ].each do |mapping|
25
+ map_record_field!(record, mapping.keys.first, mapping.values.first)
26
+ end
27
+
28
+ add_place_of_publication!(record)
29
+ end
30
+ end
31
+
32
+ #
33
+ private
34
+ #
35
+ def add_place_of_publication!(record)
36
+ if record["publisher"]
37
+ record["publisher"].match(/(\A[^:]*):(.*)/) do |match|
38
+ place_of_publication, publisher = match.captures.map(&:strip)
39
+
40
+ record["placeOfPublication"] = place_of_publication
41
+ record["publisher"].replace(publisher)
42
+ end
43
+ end
44
+ end
45
+
46
+ def add_records!(target)
47
+ (search_brief_return.locate("JAGROOT/RESULT/DOCSET/DOC") || []).each do |source_record|
48
+ target["hits"]["hits"].push({
49
+ "_type" => "record",
50
+ "_source" => source_record.locate("PrimoNMBib/record/?").inject({}) do |_source, section|
51
+ _source.merge! section.value => transformation.hash_from_ox_element(section)
52
+ end
53
+ })
54
+ end
55
+
56
+ target["hits"]["hits"] # for chaining
57
+ end
58
+
59
+ def map_record_field!(record, source_path, target_path)
60
+ source_value = resolve_path(record, source_path)
61
+ target = resolve_path(record, target_path, -2)
62
+ target_key = target_path.split("/").last
63
+
64
+ if target[target_key]
65
+ target[target_key] = [target[target_key]]
66
+ end
67
+
68
+ if target[target_key].is_a?(Array)
69
+ (target[target_key] << source_value).flatten(1)
70
+ else
71
+ target[target_key] = source_value
72
+ end
73
+ end
74
+
75
+ def resolve_path(object, path, level = -1)
76
+ path.split("/").slice(0..level).inject(object) do |_object, key|
77
+ _object[key] unless _object.nil?
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,15 @@
1
+ require "celsius/transformation/step"
2
+ require "ox"
3
+
4
+ class Celsius::Primo::SoapApi::Searcher::SearchBrief::SearchResultTransformation::
5
+ RemoveBogusCreationdateFacets < Celsius::Transformation::Step
6
+
7
+ #
8
+ # Sometimes, there are creationdate facets entries 1 and 500, which are bogus
9
+ #
10
+ def call
11
+ ((target["facets"]["facet_creationdate"] || {})["terms"] || []).reject! do |element|
12
+ element["term"] == "1" || element["term"] == "500"
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,14 @@
1
+ require "celsius/transformation/step"
2
+ require "ox"
3
+
4
+ class Celsius::Primo::SoapApi::Searcher::SearchBrief::SearchResultTransformation::
5
+ SelectOnlyRequestedFacets < Celsius::Transformation::Step
6
+
7
+ def call
8
+ if requested_facets_names = Celsius::Hash.deep_find_key(search_request, [:facets, :field])
9
+ target["facets"].select! do |target_facet_name, _|
10
+ requested_facets_names.include?(target_facet_name)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ require "celsius/transformation/step"
2
+ require "ox"
3
+
4
+ class Celsius::Primo::SoapApi::Searcher::SearchBrief::SearchResultTransformation::
5
+ SetTotalHits < Celsius::Transformation::Step
6
+
7
+ def call
8
+ if docset = (search_brief_return.locate("JAGROOT/RESULT/DOCSET") || []).first
9
+ target["hits"]["total"] = docset[:TOTALHITS].to_i
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,17 @@
1
+ require "celsius/transformation/step"
2
+ require "ox"
3
+
4
+ class Celsius::Primo::SoapApi::Searcher::SearchBrief::SearchResultTransformation::
5
+ SetupTargetSkeleton < Celsius::Transformation::Step
6
+
7
+ def call
8
+ transformation.target = {
9
+ "took" => nil,
10
+ "hits" => {
11
+ "hits" => [],
12
+ "total" => 0
13
+ },
14
+ "facets" => {}
15
+ }
16
+ end
17
+ end
@@ -0,0 +1,21 @@
1
+ require "celsius/transformation/step"
2
+ require "ox"
3
+
4
+ class Celsius::Primo::SoapApi::Searcher::SearchBrief::SearchResultTransformation::
5
+ SortFacetsBySearchRequest < Celsius::Transformation::Step
6
+
7
+ def call
8
+ if requested_facets_names = Celsius::Hash.deep_find_key(search_request, [:facets, :field])
9
+ original_target_facets = target["facets"]
10
+
11
+ # ruby has a order-preserving hash, so sorting means recreating with the desired order
12
+ target["facets"] = {}.tap do |target_facets|
13
+ requested_facets_names.each do |facet_name|
14
+ if original_target_facet_value = original_target_facets[facet_name]
15
+ target_facets[facet_name] = original_target_facet_value
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,59 @@
1
+ require "celsius/primo/soap_api/searcher/search_brief"
2
+ require "celsius/transformation"
3
+
4
+ class Celsius::Primo::SoapApi::Searcher::SearchBrief::SearchResultTransformation < Celsius::Transformation
5
+ require_directory "#{File.dirname(__FILE__)}/search_result_transformation"
6
+
7
+ sequence [
8
+ ParseInnerSearchBriefReturn,
9
+ SetupTargetSkeleton,
10
+ SetTotalHits,
11
+ [ # facets
12
+ ProcessFacets,
13
+ AddMissingFacets,
14
+ RemoveBogusCreationdateFacets,
15
+ SelectOnlyRequestedFacets,
16
+ SortFacetsBySearchRequest
17
+ ],
18
+ ProcessRecords
19
+ ]
20
+
21
+ attr_accessor :search_brief_return
22
+ attr_accessor :search_request
23
+
24
+ def initialize(options = {})
25
+ @search_request = options[:search_request]
26
+ end
27
+
28
+ def hash_from_ox_element(ox_element)
29
+ OxHelpers.hash_from_ox_element(ox_element)
30
+ end
31
+
32
+ module OxHelpers
33
+ def self.hash_from_ox_element(ox_element)
34
+ if string_element?(ox_element)
35
+ ox_element.first.force_encoding("utf-8")
36
+ else
37
+ ox_element.nodes.inject({}) do |hash, node|
38
+ key = node.value
39
+
40
+ if hash[key].is_a?(String)
41
+ hash[key] = [hash[key]]
42
+ end
43
+
44
+ if hash[key].is_a?(Array)
45
+ hash[key] << hash_from_ox_element(node.nodes)
46
+ else
47
+ hash[key] = hash_from_ox_element(node.nodes)
48
+ end
49
+
50
+ hash
51
+ end
52
+ end
53
+ end
54
+
55
+ def self.string_element?(ox_element)
56
+ ox_element.is_a?(Array) && ox_element.length == 1 && ox_element.first.is_a?(String)
57
+ end
58
+ end
59
+ end