diff --git a/index.ts b/index.ts index 9441403..aa02bfc 100644 --- a/index.ts +++ b/index.ts @@ -59,7 +59,8 @@ export type { CoordinateLicense, MatchInfo } from "./src/us_street/Candidate.js" export type { Geolocation } from "./src/us_autocomplete_pro/Lookup.js"; export type { AutocompleteSource } from "./src/us_autocomplete/Lookup.js"; export type { ReverseGeoSource } from "./src/us_reverse_geo/Lookup.js"; -export type { Language, Geocode } from "./src/international_street/Lookup.js"; +export type { Geocode } from "./src/international_street/Lookup.js"; +export { LanguageMode } from "./src/international_street/Lookup.js"; export { Batch, diff --git a/src/international_street/Client.ts b/src/international_street/Client.ts index cb4a0a1..3362189 100644 --- a/src/international_street/Client.ts +++ b/src/international_street/Client.ts @@ -4,7 +4,7 @@ import Candidate, { RawIntlStreetCandidate } from "./Candidate.js"; import buildInputData from "../util/buildInputData.js"; import apiToSDKKeyMap from "../util/apiToSDKKeyMap.js"; import { Sender, Response } from "../types.js"; -import Lookup from "./Lookup.js"; +import Lookup, { resolveLanguageMode } from "./Lookup.js"; const keyTranslationFormat = apiToSDKKeyMap.internationalStreet; @@ -19,10 +19,14 @@ export default class Client { if (typeof lookup === "undefined") throw new UndefinedLookupError(); lookup.ensureEnoughInfo(); + lookup.ensureValidData(); const request = new Request(); request.parameters = buildInputData(lookup, keyTranslationFormat); + const resolvedLanguage = resolveLanguageMode(lookup.language); + if (resolvedLanguage !== undefined) request.parameters["language"] = resolvedLanguage; + return new Promise((resolve, reject) => { this.sender .send(request) diff --git a/src/international_street/Lookup.ts b/src/international_street/Lookup.ts index 2fb7828..5caec52 100644 --- a/src/international_street/Lookup.ts +++ b/src/international_street/Lookup.ts @@ -1,7 +1,11 @@ import Candidate from "./Candidate.js"; import { UnprocessableEntityError } from "../Errors.js"; -export type Language = "native" | "latin" | (string & {}); +export enum LanguageMode { + Native = "native", + Latin = "latin", +} + export type Geocode = "true" | (string & {}); const messages = { @@ -24,6 +28,22 @@ function fieldIsSet(field: string | undefined): boolean { return !fieldIsMissing(field); } +// Resolves a LanguageMode member or a raw value (eg. from untyped JS callers) into a LanguageMode, +// matching "native"/"latin" regardless of case. Returns undefined for unset/empty input. +export function resolveLanguageMode( + value: LanguageMode | string | undefined, +): LanguageMode | undefined { + if (!fieldIsSet(value)) return undefined; + + const match = Object.values(LanguageMode).find( + (mode) => mode.toLowerCase() === String(value).toLowerCase(), + ); + + if (!match) throw new UnprocessableEntityError(messages.invalidLanguage); + + return match; +} + export default class Lookup { result: Candidate[]; country: string | undefined; @@ -37,7 +57,7 @@ export default class Lookup { administrativeArea: string | undefined; postalCode: string | undefined; geocode: Geocode | undefined; - language: Language | undefined; + language: LanguageMode | undefined; inputId: string | undefined; customParameters: Record; @@ -77,19 +97,13 @@ export default class Lookup { } ensureValidData(): boolean { - const languageIsSetIncorrectly = () => { - const isLanguage = (language: string) => this.language!.toLowerCase() === language; - - return fieldIsSet(this.language) && !(isLanguage("latin") || isLanguage("native")); - }; - const geocodeIsSetIncorrectly = () => { return fieldIsSet(this.geocode) && this.geocode!.toLowerCase() !== "true"; }; if (geocodeIsSetIncorrectly()) throw new UnprocessableEntityError(messages.badGeocode); - if (languageIsSetIncorrectly()) throw new UnprocessableEntityError(messages.invalidLanguage); + resolveLanguageMode(this.language); return true; } diff --git a/tests/international_street/test_Client.ts b/tests/international_street/test_Client.ts index 7e987a6..809bd1c 100644 --- a/tests/international_street/test_Client.ts +++ b/tests/international_street/test_Client.ts @@ -1,6 +1,6 @@ import { expect } from "chai"; import Client from "../../src/international_street/Client.js"; -import Lookup from "../../src/international_street/Lookup.js"; +import Lookup, { LanguageMode } from "../../src/international_street/Lookup.js"; import Candidate from "../../src/international_street/Candidate.js"; import errors from "../../src/Errors.js"; import { MockSender, MockSenderWithResponse } from "../fixtures/mock_senders.js"; @@ -45,8 +45,8 @@ describe("An International Street client", function () { lookup.locality = "h"; lookup.administrativeArea = "i"; lookup.postalCode = "j"; - lookup.geocode = "k"; - lookup.language = "l"; + lookup.geocode = "true"; + lookup.language = LanguageMode.Latin; let expectedParameters = { country: "a", freeform: "b", @@ -58,8 +58,8 @@ describe("An International Street client", function () { locality: "h", administrative_area: "i", postal_code: "j", - geocode: "k", - language: "l", + geocode: "true", + language: "latin", }; client.send(lookup); @@ -67,6 +67,28 @@ describe("An International Street client", function () { expect(mockSender.request.parameters).to.deep.equal(expectedParameters); }); + it("normalizes a mixed-case language value before sending.", function () { + let mockSender = new MockSender(); + let client = new Client(mockSender); + let lookup = new Lookup("CA", "123 Main St"); + lookup.language = "Latin" as LanguageMode; + + client.send(lookup); + + expect(mockSender.request.parameters["language"]).to.equal("latin"); + }); + + it("does not mutate the original language value when normalizing.", function () { + let mockSender = new MockSender(); + let client = new Client(mockSender); + let lookup = new Lookup("CA", "123 Main St"); + lookup.language = "Latin" as LanguageMode; + + client.send(lookup); + + expect(lookup.language).to.equal("Latin"); + }); + it("attaches a match candidate from a response to a lookup.", function () { const expectedMockPayload = [{ address1: "A" }]; let mockSender = new MockSenderWithResponse(expectedMockPayload); diff --git a/tests/international_street/test_Lookup.ts b/tests/international_street/test_Lookup.ts index 18a4495..1f59512 100644 --- a/tests/international_street/test_Lookup.ts +++ b/tests/international_street/test_Lookup.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import Lookup from "../../src/international_street/Lookup.js"; +import Lookup, { LanguageMode } from "../../src/international_street/Lookup.js"; import errors from "../../src/Errors.js"; describe("An International Street lookup", function () { @@ -35,7 +35,7 @@ describe("An International Street lookup", function () { it("rejects lookups with an invalid language.", function () { let lookup = new Lookup(); - lookup.language = "Rubberduckian"; + lookup.language = "Rubberduckian" as LanguageMode; ensureValidationThrows(lookup.ensureValidData, messages.invalidLanguage); }); @@ -52,16 +52,23 @@ describe("An International Street lookup", function () { it("accepts lookups with a valid language.", function () { let lookup1 = new Lookup(); - lookup1.language = "latin"; + lookup1.language = LanguageMode.Latin; expect(lookup1.ensureValidData()).to.equal(true); let lookup2 = new Lookup(); - lookup2.language = "native"; + lookup2.language = LanguageMode.Native; expect(lookup2.ensureValidData()).to.equal(true); }); + it("accepts lookups with a mixed-case language value.", function () { + let lookup = new Lookup(); + lookup.language = "Latin" as LanguageMode; + + expect(lookup.ensureValidData()).to.equal(true); + }); + function ensureValidationThrows(callback: any, message: any) { let expectedError = new errors.UnprocessableEntityError(message);