diff --git a/lib/collectionspace/mapper/searcher.rb b/lib/collectionspace/mapper/searcher.rb index 15edfcda..2127b2c9 100644 --- a/lib/collectionspace/mapper/searcher.rb +++ b/lib/collectionspace/mapper/searcher.rb @@ -21,16 +21,13 @@ def call(value:, type:, subtype: nil) attr_reader :client, :active, :search_fields - def case_swap(string) - string.match?(/[A-Z]/) ? string.downcase : string.capitalize - end - - def get_response(value, type, subtype) + def get_response(value, type, subtype, operator = "=") response = client.find( type: type, subtype: subtype, value: value, - field: search_field(type) + field: search_field(type), + operator: operator ) rescue => e puts e.message @@ -50,6 +47,10 @@ def lookup_search_field(type) def parse_response(response) parsed = response.parsed["abstract_common_list"] + return parsed if parsed["list_item"].is_a?(Array) + + parsed["list_item"] = [parsed["list_item"]] + parsed rescue => e puts e.message nil @@ -83,7 +84,21 @@ def search_response(value, type, subtype) as_is = get_response(value, type, subtype) return as_is if response_usable?(as_is) - get_response(case_swap(value), type, subtype) + case_insensitive_response(value, type, subtype) + end + + def case_insensitive_response(value, type, subtype) + response = get_response(value, type, subtype, "ILIKE") + return nil unless response_usable?(response) + + displayname = response.dig("list_item", 0, "displayName") || + response.dig("list_item", 0, "termDisplayName") + warning = { + category: "case_insensitive_match", + message: "Searched: #{value}. Using: #{displayname}" + } + response["warnings"] = [warning] + response end end end diff --git a/lib/collectionspace/mapper/term_searchable.rb b/lib/collectionspace/mapper/term_searchable.rb index d97b5c30..0def4229 100644 --- a/lib/collectionspace/mapper/term_searchable.rb +++ b/lib/collectionspace/mapper/term_searchable.rb @@ -194,13 +194,20 @@ def rec_from_response(category, val, apiresponse) end def return_record(category, val, apiresponse, term_ct) + rec = apiresponse["list_item"][0] + case term_ct when 0 - rec = nil + nil when 1 - rec = apiresponse["list_item"] + if apiresponse.key?("warnings") + apiresponse["warnings"].each do |warning| + response.add_warning(warning.merge({field: column})) + end + end + rec + # rec = apiresponse["list_item"] else - rec = apiresponse["list_item"][0] using_uri = "#{client.config.base_uri}#{rec["uri"]}" response.add_warning({ category: :"multiple_records_found_for_#{category}", @@ -210,9 +217,8 @@ def return_record(category, val, apiresponse, term_ct) value: val, message: "#{term_ct} records found. Using #{using_uri}" }) + rec end - - rec end def add_bad_lookup_error(category, val) diff --git a/spec/collectionspace/mapper/searcher_spec.rb b/spec/collectionspace/mapper/searcher_spec.rb index d97f34d2..2f21a348 100644 --- a/spec/collectionspace/mapper/searcher_spec.rb +++ b/spec/collectionspace/mapper/searcher_spec.rb @@ -15,44 +15,48 @@ describe "#.call", vcr: "core_domain_check" do let(:result) { searcher.call(**args) } - let(:args) { {value: "All", type: "vocabularies", subtype: "publishto"} } + let(:args) { {value: value, type: "vocabularies", subtype: "publishto"} } context "when search_if_not_cached = true", vcr: "searcher_search" do let(:config) { {} } + let(:value) { "All" } it "returns expected hash" do - expected = { - "fieldsReturned" => - "csid|uri|refName|updatedAt|workflowState|rev|sourcePage|sas|"\ - "proposed|referenced|deprecated|termStatus|description|source|"\ - "order|displayName|shortIdentifier", - "itemsInPage" => "1", - "list_item" => { - "csid" => "d614ebc5-96fd-4680-9727", - "displayName" => "All", - "proposed" => "true", - "refName" => - "urn:cspace:core.collectionspace.org:vocabularies:name"\ - "(publishto):item:name(all)'All'", - "rev" => "0", - "sas" => "false", - "shortIdentifier" => "all", - "updatedAt" => "2020-02-08T03:30:26.054Z", - "uri" => - "/vocabularies/e2ea6ca3-4c60-427d-96e5/items/"\ - "d614ebc5-96fd-4680-9727", - "workflowState" => "project" - }, - "pageNum" => "0", - "pageSize" => "25", - "totalItems" => "1" - } - expect(result).to eq(expected) + expect(result["totalItems"]).to eq("1") + expect(result.dig("list_item", 0, "displayName")).to eq("All") + end + + context "when term not matching case sensitively" do + context "when vocabulary", vcr: "searcher_ci_vocab" do + let(:value) { "alL" } + + it "returns expected hash" do + expect(result.key?("warnings")).to be true + warning = result["warnings"].first + expect(warning[:category]).to eq("case_insensitive_match") + expect(warning[:message]).to eq("Searched: #{value}. Using: All") + end + end + + context "when authority", vcr: "searcher_ci_authority" do + let(:value) { "Art" } + let(:args) do + {value: value, type: "conceptauthorities", subtype: "concept"} + end + + it "returns expected hash" do + expect(result.key?("warnings")).to be true + warning = result["warnings"].first + expect(warning[:category]).to eq("case_insensitive_match") + expect(warning[:message]).to eq("Searched: #{value}. Using: art") + end + end end end context "when search_if_not_cached = false" do let(:config) { {search_if_not_cached: false} } + let(:value) { "All" } it "returns nil" do expect(result).to be_nil diff --git a/spec/collectionspace/mapper/term_searchable_spec.rb b/spec/collectionspace/mapper/term_searchable_spec.rb index a336799f..917be6c5 100644 --- a/spec/collectionspace/mapper/term_searchable_spec.rb +++ b/spec/collectionspace/mapper/term_searchable_spec.rb @@ -7,13 +7,16 @@ class TermClass include CollectionSpace::Mapper::TermSearchable - attr_reader :type, :subtype, :handler + attr_reader :type, :subtype, :handler, :response def initialize(type, subtype, handler) @type = type @subtype = subtype @handler = handler + @response = CollectionSpace::Mapper::Response.new({}, handler) @errors = [] end + + def column = "foo" end RSpec.describe CollectionSpace::Mapper::TermSearchable, @@ -131,6 +134,11 @@ def initialize(type, subtype, handler) expected = "urn:cspace:core.collectionspace.org:vocabularies:name"\ "(publishto):item:name(all)'All'" expect(result).to eq(expected) + expect(term.response.warnings).to include({ + category: "case_insensitive_match", + message: "Searched: all. Using: All", + field: "foo" + }) end end end diff --git a/spec/support/cassettes/searcher_ci_authority.yml b/spec/support/cassettes/searcher_ci_authority.yml new file mode 100644 index 00000000..c74d01ac --- /dev/null +++ b/spec/support/cassettes/searcher_ci_authority.yml @@ -0,0 +1,167 @@ +--- +http_interactions: +- request: + method: get + uri: https://core.dev.collectionspace.org/cspace-services/personauthorities?pgNum=0&pgSz=1&wf_deleted=false + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Authorization: + - Basic YWRtaW5AY29yZS5jb2xsZWN0aW9uc3BhY2Uub3JnOkFkbWluaXN0cmF0b3I= + response: + status: + code: 200 + message: '' + headers: + Date: + - Sat, 19 Oct 2024 00:12:24 GMT + Content-Type: + - application/xml + Content-Length: + - '805' + Connection: + - keep-alive + Vary: + - Access-Control-Request-Headers + - Access-Control-Request-Method + - Origin + Set-Cookie: + - JSESSIONID=FEBAA20BE92073180EE63DDEC3490877; Path=/cspace-services; Secure; + HttpOnly + X-Content-Type-Options: + - nosniff + X-Xss-Protection: + - 1; mode=block + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Pragma: + - no-cache + Expires: + - '0' + Strict-Transport-Security: + - max-age=31536000 ; includeSubDomains + X-Frame-Options: + - DENY + body: + encoding: UTF-8 + string: 0112csid|uri|refName|updatedAt|workflowState|rev|shortIdentifier|sas|displayName|vocabType4bea28cb-83be-4ca1-971b/personauthorities/4bea28cb-83be-4ca1-971burn:cspace:core.collectionspace.org:personauthorities:name(person)'Local + Persons'2023-08-26T05:44:37.565Zproject13personLocal + PersonsPersonAuthority + recorded_at: Sat, 19 Oct 2024 00:12:24 GMT +- request: + method: get + uri: https://core.dev.collectionspace.org/cspace-services/conceptauthorities/urn:cspace:name(concept)/items?as=concepts_common:conceptTermGroupList/0/termDisplayName%20=%20%27Art%27&pgSz=25&sortBy=collectionspace_core:updatedAt%20DESC&wf_deleted=false + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Authorization: + - Basic YWRtaW5AY29yZS5jb2xsZWN0aW9uc3BhY2Uub3JnOkFkbWluaXN0cmF0b3I= + response: + status: + code: 200 + message: '' + headers: + Date: + - Sat, 19 Oct 2024 00:12:25 GMT + Content-Type: + - application/xml + Content-Length: + - '402' + Connection: + - keep-alive + Vary: + - Access-Control-Request-Headers + - Access-Control-Request-Method + - Origin + Set-Cookie: + - JSESSIONID=A5EB36E7ED1BEEB2C121BD38D3CFD55A; Path=/cspace-services; Secure; + HttpOnly + X-Content-Type-Options: + - nosniff + X-Xss-Protection: + - 1; mode=block + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Pragma: + - no-cache + Expires: + - '0' + Strict-Transport-Security: + - max-age=31536000 ; includeSubDomains + X-Frame-Options: + - DENY + body: + encoding: UTF-8 + string: 02500csid|uri|refName|updatedAt|workflowState|rev|shortIdentifier|sas|proposed|deprecated|termStatus|termDisplayName + recorded_at: Sat, 19 Oct 2024 00:12:25 GMT +- request: + method: get + uri: https://core.dev.collectionspace.org/cspace-services/conceptauthorities/urn:cspace:name(concept)/items?as=concepts_common:conceptTermGroupList/0/termDisplayName%20ILIKE%20%27Art%27&pgSz=25&sortBy=collectionspace_core:updatedAt%20DESC&wf_deleted=false + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Authorization: + - Basic YWRtaW5AY29yZS5jb2xsZWN0aW9uc3BhY2Uub3JnOkFkbWluaXN0cmF0b3I= + response: + status: + code: 200 + message: '' + headers: + Date: + - Sat, 19 Oct 2024 00:12:25 GMT + Content-Type: + - application/xml + Content-Length: + - '892' + Connection: + - keep-alive + Vary: + - Access-Control-Request-Headers + - Access-Control-Request-Method + - Origin + Set-Cookie: + - JSESSIONID=CF61C02A1720ED8CFAE1B82E1F0C7CFE; Path=/cspace-services; Secure; + HttpOnly + X-Content-Type-Options: + - nosniff + X-Xss-Protection: + - 1; mode=block + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Pragma: + - no-cache + Expires: + - '0' + Strict-Transport-Security: + - max-age=31536000 ; includeSubDomains + X-Frame-Options: + - DENY + body: + encoding: UTF-8 + string: 02511csid|uri|refName|updatedAt|workflowState|rev|shortIdentifier|sas|proposed|deprecated|termStatus|termDisplayName69b1a466-0c91-4baf-868c/conceptauthorities/92e3f0a0-2fa2-4ecb-a3f7/items/69b1a466-0c91-4baf-868curn:cspace:core.collectionspace.org:conceptauthorities:name(concept):item:name(art1729294383466)'art'2024-10-18T23:33:03.465Zproject0art1729294383466falsetrueart + recorded_at: Sat, 19 Oct 2024 00:12:25 GMT +recorded_with: VCR 6.1.0 diff --git a/spec/support/cassettes/searcher_ci_vocab.yml b/spec/support/cassettes/searcher_ci_vocab.yml new file mode 100644 index 00000000..b2e47791 --- /dev/null +++ b/spec/support/cassettes/searcher_ci_vocab.yml @@ -0,0 +1,167 @@ +--- +http_interactions: +- request: + method: get + uri: https://core.dev.collectionspace.org/cspace-services/personauthorities?pgNum=0&pgSz=1&wf_deleted=false + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Authorization: + - Basic YWRtaW5AY29yZS5jb2xsZWN0aW9uc3BhY2Uub3JnOkFkbWluaXN0cmF0b3I= + response: + status: + code: 200 + message: '' + headers: + Date: + - Sat, 19 Oct 2024 00:09:32 GMT + Content-Type: + - application/xml + Content-Length: + - '805' + Connection: + - keep-alive + Vary: + - Access-Control-Request-Headers + - Access-Control-Request-Method + - Origin + Set-Cookie: + - JSESSIONID=E723F5E64299E095C333FC7B57C0EB1E; Path=/cspace-services; Secure; + HttpOnly + X-Content-Type-Options: + - nosniff + X-Xss-Protection: + - 1; mode=block + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Pragma: + - no-cache + Expires: + - '0' + Strict-Transport-Security: + - max-age=31536000 ; includeSubDomains + X-Frame-Options: + - DENY + body: + encoding: UTF-8 + string: 0112csid|uri|refName|updatedAt|workflowState|rev|shortIdentifier|sas|displayName|vocabType4bea28cb-83be-4ca1-971b/personauthorities/4bea28cb-83be-4ca1-971burn:cspace:core.collectionspace.org:personauthorities:name(person)'Local + Persons'2023-08-26T05:44:37.565Zproject13personLocal + PersonsPersonAuthority + recorded_at: Sat, 19 Oct 2024 00:09:32 GMT +- request: + method: get + uri: https://core.dev.collectionspace.org/cspace-services/vocabularies/urn:cspace:name(publishto)/items?as=vocabularyitems_common:displayName%20=%20%27alL%27&pgSz=25&sortBy=collectionspace_core:updatedAt%20DESC&wf_deleted=false + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Authorization: + - Basic YWRtaW5AY29yZS5jb2xsZWN0aW9uc3BhY2Uub3JnOkFkbWluaXN0cmF0b3I= + response: + status: + code: 200 + message: '' + headers: + Date: + - Sat, 19 Oct 2024 00:09:33 GMT + Content-Type: + - application/xml + Content-Length: + - '445' + Connection: + - keep-alive + Vary: + - Access-Control-Request-Headers + - Access-Control-Request-Method + - Origin + Set-Cookie: + - JSESSIONID=5703A73AE43342059484EF6DE4077EF0; Path=/cspace-services; Secure; + HttpOnly + X-Content-Type-Options: + - nosniff + X-Xss-Protection: + - 1; mode=block + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Pragma: + - no-cache + Expires: + - '0' + Strict-Transport-Security: + - max-age=31536000 ; includeSubDomains + X-Frame-Options: + - DENY + body: + encoding: UTF-8 + string: 02500csid|uri|refName|updatedAt|workflowState|rev|sourcePage|sas|proposed|referenced|deprecated|termStatus|description|source|order|displayName|shortIdentifier + recorded_at: Sat, 19 Oct 2024 00:09:33 GMT +- request: + method: get + uri: https://core.dev.collectionspace.org/cspace-services/vocabularies/urn:cspace:name(publishto)/items?as=vocabularyitems_common:displayName%20ILIKE%20%27alL%27&pgSz=25&sortBy=collectionspace_core:updatedAt%20DESC&wf_deleted=false + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Authorization: + - Basic YWRtaW5AY29yZS5jb2xsZWN0aW9uc3BhY2Uub3JnOkFkbWluaXN0cmF0b3I= + response: + status: + code: 200 + message: '' + headers: + Date: + - Sat, 19 Oct 2024 00:09:33 GMT + Content-Type: + - application/xml + Content-Length: + - '891' + Connection: + - keep-alive + Vary: + - Access-Control-Request-Headers + - Access-Control-Request-Method + - Origin + Set-Cookie: + - JSESSIONID=12AC3BF6A209CF8FC3622F4212CF4258; Path=/cspace-services; Secure; + HttpOnly + X-Content-Type-Options: + - nosniff + X-Xss-Protection: + - 1; mode=block + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Pragma: + - no-cache + Expires: + - '0' + Strict-Transport-Security: + - max-age=31536000 ; includeSubDomains + X-Frame-Options: + - DENY + body: + encoding: UTF-8 + string: 02511csid|uri|refName|updatedAt|workflowState|rev|sourcePage|sas|proposed|referenced|deprecated|termStatus|description|source|order|displayName|shortIdentifier1be6baa8-3691-4fad-bf01/vocabularies/8fd03497-36d2-4d91-a46b/items/1be6baa8-3691-4fad-bf01urn:cspace:core.collectionspace.org:vocabularies:name(publishto):item:name(all)'All'2023-08-26T05:43:29.142Zproject0falsetrueAllall + recorded_at: Sat, 19 Oct 2024 00:09:33 GMT +recorded_with: VCR 6.1.0