Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 24 additions & 22 deletions library/languages.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const fs = require('fs');
const os = require('os');
const {validateParameter, validateOptionalParameter, Utilities} = require("./utilities");
const {validateOptionalParameter, Utilities} = require("./utilities");

/**
* Language part types for matching depth
Expand Down Expand Up @@ -120,7 +120,7 @@ class Language {
if (index < parts.length) {
this.language = parts[index];
if (this.language != '*' && languageDefinitions && !languageDefinitions.languages.has(this.language)) {
throw new Error("The language '"+this.language+"' in the code '"+this.language+"' is not valid");
throw new Error("The language '"+this.language+"' in the code '"+this.code+"' is not valid");
}
index++;
}
Expand All @@ -130,7 +130,7 @@ class Language {
const part = parts[index];
if (part.length === 3 && /^[a-zA-Z]{3}$/.test(part)) {
if (languageDefinitions && !languageDefinitions.extLanguages.has(part)) {
throw new Error("The extLanguage '"+part+"' in the code '"+code+"' is not valid");
throw new Error("The extLanguage '"+part+"' in the code '"+this.code+"' is not valid");
}
this.extLang.push(part.toLowerCase());
index++;
Expand All @@ -144,7 +144,7 @@ class Language {
const part = parts[index];
if (part.length === 4 && /^[a-zA-Z]{4}$/.test(part)) {
if (languageDefinitions && !languageDefinitions.scripts.has(part)) {
throw new Error("The script '"+part+"' in the code '"+code+"' is not valid");
throw new Error("The script '"+part+"' in the code '"+this.code+"' is not valid");
}
this.script = part.charAt(0).toUpperCase() + part.slice(1).toLowerCase();
index++;
Expand All @@ -157,7 +157,7 @@ class Language {
if ((part.length === 2 && /^[a-zA-Z]{2}$/.test(part)) ||
(part.length === 3 && /^[0-9]{3}$/.test(part))) {
if (languageDefinitions && !languageDefinitions.regions.has(part)) {
throw new Error("The region '"+part+"' in the code '"+code+"' is not valid");
throw new Error("The region '"+part+"' in the code '"+this.code+"' is not valid");
}
this.region = part.toUpperCase();
index++;
Expand Down Expand Up @@ -187,9 +187,10 @@ class Language {
} else if (part.length === 1 && part !== 'x') {
// Extension
this.extension = part + '-' + parts.slice(index + 1).join('-');
index++;
break;
} else {
index++;
throw new Error("Unable to recognised '"+parts[index]+"' as a valid part in the language code "+this.code);
}
}
}
Expand Down Expand Up @@ -671,29 +672,30 @@ class LanguageDefinitions {
*
* @return {Language} parsed language (or null)
*/
parse(code) {
if (!code) return null;
parse(code, msg) {
if (!code) {
if (msg) {
msg.message = 'No code provided';
}
return null;
}

// Check cache first
if (this.parsed.has(code)) {
return this.parsed.get(code);
}

const parts = code.split('-');
let index = 0;

// Validate language
if (index >= parts.length) return null;
const langCode = parts[index].toLowerCase();
if (!this.languages.has(langCode) && langCode !== '*') {
return null; // Invalid language code
try {
const lang = new Language(code, this);
// Cache the result
this.parsed.set(code, lang);
return lang;
} catch (e) {
if (msg) {
msg.message = e.message;
}
return null;
}

const lang = new Language(code);

// Cache the result
this.parsed.set(code, lang);
return lang;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion tests/cs/cs-country.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ describe('CountryCodeServices', () => {
});

test('should return error for invalid codes', async () => {
const invalidCodes = ['XX', 'ZZZ', '999'];
const invalidCodes = ['XX-XX', 'ZZZ', '999'];

for (const code of invalidCodes) {
const result = await provider.locate(code);
Expand Down
87 changes: 83 additions & 4 deletions tests/library/languages.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ const path = require('path');
const {TestUtilities} = require("../test-utilities");

describe('Language Class', () => {
let languageDefinitions;

beforeEach(async () => {
this.languageDefinitions = await TestUtilities.loadLanguageDefinitions();
});
Expand Down Expand Up @@ -216,7 +214,7 @@ describe('Languages Class', () => {
});

describe('Language Iteration and Access', () => {
test('should be iterable', () => {
test('should be iteratable', () => {
const languages = Languages.fromAcceptLanguage('en-US,fr-CA', this.languageDefinitions);
const codes = [];
for (const lang of languages) {
Expand Down Expand Up @@ -361,8 +359,89 @@ Description: Test
});

test('should return null for invalid language codes', () => {
const lang = definitions.parse('invalid-US');
let lang = definitions.parse('invalid-US');
expect(lang).toBeNull();
const msg = {};
lang = definitions.parse('en-us', msg);
expect(lang).toBeNull();
expect(msg.message).toBe("The region 'us' in the code 'en-us' is not valid");
});

test('should parse basic language codes', () => {
const msg = {};
const lang = definitions.parse('en', msg);
expect(lang).not.toBeNull();
expect(lang.language).toBe('en');
expect(msg.message).toBeUndefined();
});

test('should parse wildcard', () => {
const lang = definitions.parse('*', {});
expect(lang).not.toBeNull();
expect(lang.language).toBe('*');
});

test('should parse language + region', () => {
const lang = definitions.parse('en-US', {});
expect(lang).not.toBeNull();
expect(lang.language).toBe('en');
expect(lang.region).toBe('US');
});

test('should parse language + script', () => {
const lang = definitions.parse('zh-Hans', {});
expect(lang).not.toBeNull();
expect(lang.language).toBe('zh');
expect(lang.script).toBe('Hans');
});

test('should parse language + script + region', () => {
const lang = definitions.parse('zh-Hans-CN', {});
expect(lang).not.toBeNull();
expect(lang.language).toBe('zh');
expect(lang.script).toBe('Hans');
expect(lang.region).toBe('CN');
});

test('should parse language + region + variant', () => {
const lang = definitions.parse('sl-IT-nedis', {});
expect(lang).not.toBeNull();
expect(lang.language).toBe('sl');
expect(lang.region).toBe('IT');
expect(lang.variant).toBe('nedis');
});

test('should parse private use extensions', () => {
const lang = definitions.parse('en-US-x-twain', {});
expect(lang).not.toBeNull();
expect(lang.language).toBe('en');
expect(lang.region).toBe('US');
});

test('should return null for empty code', () => {
const msg = {};
const lang = definitions.parse('', msg);
expect(lang).toBeNull();
});

test('should return null for invalid language code', () => {
const msg = {};
const lang = definitions.parse('xx', msg);
expect(lang).toBeNull();
expect(msg.message).toContain('not valid');
});

test('should return null for unrecognised trailing parts', () => {
const msg = {};
const lang = definitions.parse('en-ZZ-ZZ', msg);
expect(lang).toBeNull();
expect(msg.message).toContain('Unable to recognise');
});

test('should cache parsed results', () => {
const a = definitions.parse('en', {});
const b = definitions.parse('en', {});
expect(a).toBe(b);
});

test('should cache parsed languages', () => {
Expand Down
2 changes: 1 addition & 1 deletion tests/tx/designations.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ describe('Designations', () => {

test('should find preferred designation without language list', () => {
const preferred = designations.preferredDesignation();
expect(preferred.display).toBe('Base English');
expect(preferred.display).toBe('US English Display');
expect(designations.isDisplay(preferred)).toBe(true);
});

Expand Down
2 changes: 1 addition & 1 deletion tests/tx/expand.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ describe('Expand Worker', () => {
// Currently returns 200 with empty expansion (stub behavior)
// When doExpand is implemented, this should return an error
// because the CodeSystem can't be found
expect(response.status).toBe(404);
expect(response.status).toBe(422);
// expect(response.status).toBe(404); // or 400 when fully implemented
});
});
Expand Down
4 changes: 2 additions & 2 deletions tests/tx/lookup.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ describe('Lookup Worker', () => {
expect(response.body.issue[0].code).toBe('not-found');
});

test('should return 404 when system not found', async () => {
test('should return 422 when system not found', async () => {
const response = await request(app)
.get('/tx/r5/CodeSystem/$lookup')
.query({
Expand All @@ -107,7 +107,7 @@ describe('Lookup Worker', () => {
})
.set('Accept', 'application/json');

expect(response.status).toBe(404);
expect(response.status).toBe(422);
expect(response.body.resourceType).toBe('OperationOutcome');
});

Expand Down
4 changes: 2 additions & 2 deletions tests/tx/subsumes.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ describe('Subsumes Worker', () => {
expect(response.body.resourceType).toBe('OperationOutcome');
});

test('should return 404 when system not found', async () => {
test('should return 422 when system not found', async () => {
const response = await request(app)
.get('/tx/r5/CodeSystem/$subsumes')
.query({
Expand All @@ -88,7 +88,7 @@ describe('Subsumes Worker', () => {
})
.set('Accept', 'application/json');

expect(response.status).toBe(404);
expect(response.status).toBe(422);
expect(response.body.resourceType).toBe('OperationOutcome');
});

Expand Down
46 changes: 46 additions & 0 deletions tx/cs/cs-country.js
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,52 @@ class CountryCodeFactoryProvider extends CodeSystemFactoryProvider {
['ZM', 'Zambia'],
['ZW', 'Zimbabwe'],

// ISO 3166-1 User-assigned code elements
// These codes are reserved for user assignment and will never be used for country names
// See: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#User-assigned_code_elements
['AA', 'User-assigned'],
['QM', 'User-assigned'],
['QN', 'User-assigned'],
['QO', 'User-assigned'],
['QP', 'User-assigned'],
['QQ', 'User-assigned'],
['QR', 'User-assigned'],
['QS', 'User-assigned'],
['QT', 'User-assigned'],
['QU', 'User-assigned'],
['QV', 'User-assigned'],
['QW', 'User-assigned'],
['QX', 'User-assigned'],
['QY', 'User-assigned'],
['QZ', 'User-assigned'],
['XA', 'User-assigned'],
['XB', 'User-assigned'],
['XC', 'User-assigned'],
['XD', 'User-assigned'],
['XE', 'User-assigned'],
['XF', 'User-assigned'],
['XG', 'User-assigned'],
['XH', 'User-assigned'],
['XI', 'User-assigned'],
['XJ', 'User-assigned'],
['XK', 'Kosovo'],
['XL', 'User-assigned'],
['XM', 'User-assigned'],
['XN', 'User-assigned'],
['XO', 'User-assigned'],
['XP', 'User-assigned'],
['XQ', 'User-assigned'],
['XR', 'User-assigned'],
['XS', 'User-assigned'],
['XT', 'User-assigned'],
['XU', 'User-assigned'],
['XV', 'User-assigned'],
['XW', 'User-assigned'],
['XX', 'Unknown'],
['XY', 'User-assigned'],
['XZ', 'International Waters'],
['ZZ', 'Unknown or Invalid Territory'],

// 3-letter codes
['ABW', 'Aruba'],
['AFG', 'Afghanistan'],
Expand Down
5 changes: 4 additions & 1 deletion tx/cs/cs-cpt.js
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,9 @@ class CPTServices extends CodeSystemProvider {

}

isNotClosed() {
return true;
}
async extendLookup(ctxt, props, params) {
validateArrayParameter(props, 'props', String);
validateArrayParameter(params, 'params', Object);
Expand Down Expand Up @@ -545,7 +548,7 @@ class CPTServices extends CodeSystemProvider {
if (concept) {
return concept;
}
return `Code ${code} is not in the specified filter`;
return null;
}

async filterCheck(filterContext, set, concept) {
Expand Down
6 changes: 6 additions & 0 deletions tx/cs/cs-ndc.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ class NdcServices extends CodeSystemProvider {
return ctxt ? !ctxt.active : false;
}

async getStatus(code) {

const ctxt = await this.#ensureContext(code);
return ctxt.active ? "active" : "inactive";
}

async isDeprecated(code) {
await this.#ensureContext(code);
return false; // NDC doesn't track deprecated status separately
Expand Down
2 changes: 1 addition & 1 deletion tx/cs/cs-rxnorm.js
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@ class RxNormServices extends CodeSystemProvider {
if (err) {
reject(err);
} else if (!row) {
resolve(`Code ${code} is not in the specified filter`);
resolve(null);
} else {
const concept = new RxNormConcept(row[this.getCodeField()], row.STR);
resolve(concept);
Expand Down
4 changes: 2 additions & 2 deletions tx/cs/cs-snomed.js
Original file line number Diff line number Diff line change
Expand Up @@ -626,7 +626,7 @@ class SnomedProvider extends CodeSystemProvider {
} catch (error) {
return {
context: null,
message: `Code ${code} is not a valid SNOMED CT Term, and could not be parsed as an expression (${error.message})`
message: `Code ${code} is not a valid SNOMED CT Term, and neither could it be parsed as an expression (${error.message})`
};
}
} else {
Expand Down Expand Up @@ -848,7 +848,7 @@ class SnomedProvider extends CodeSystemProvider {
if (found) {
return ctxt;
} else {
return `Code ${code} is not in the specified filter`;
return null;
}
}

Expand Down
Loading