diff --git a/src/cli/args.ts b/src/cli/args.ts index ff1295f..1094ce7 100644 --- a/src/cli/args.ts +++ b/src/cli/args.ts @@ -19,6 +19,7 @@ import {ArgumentParser} from 'argparse'; export interface Options { /** HTTPS URL to an .nt file defining a custom ontology. */ ontology: string; + file: string | undefined; verbose: boolean; deprecated: boolean; context: string; @@ -78,6 +79,11 @@ export function ParseFlags(args?: string[]): Options { metavar: 'https://url.to/schema.nt', dest: 'ontology', }); + parser.add_argument('--file', { + default: undefined, + help: 'file path to a .nt file, for using a local ontology file', + dest: 'file', + }); const deprecated = parser.add_mutually_exclusive_group({required: false}); deprecated.add_argument('--deprecated', { diff --git a/src/cli/internal/main.ts b/src/cli/internal/main.ts index 110d41f..3781287 100644 --- a/src/cli/internal/main.ts +++ b/src/cli/internal/main.ts @@ -14,9 +14,11 @@ * limitations under the License. */ +import {Observable} from 'rxjs'; +import {Triple} from '../..'; import {Log, SetOptions} from '../../logging'; import {WriteDeclarations} from '../../transform/transform'; -import {load} from '../../triples/reader'; +import {load, loadFile} from '../../triples/reader'; import {Context} from '../../ts/context'; import {ParseFlags} from '../args'; @@ -26,9 +28,16 @@ export async function main(args?: string[]) { SetOptions(options); const ontologyUrl = options.ontology; - Log(`Loading Ontology from URL: ${ontologyUrl}`); + const filePath = options.file; + let result: Observable; - const result = load(ontologyUrl); + if (filePath) { + Log(`Loading Ontology from path: ${filePath}`); + result = loadFile(filePath); + } else { + Log(`Loading Ontology from URL: ${ontologyUrl}`); + result = load(ontologyUrl); + } const context = Context.Parse(options.context); await WriteDeclarations(result, options.deprecated, context, write); } diff --git a/src/triples/reader.ts b/src/triples/reader.ts index 6ee5f29..afca9a5 100644 --- a/src/triples/reader.ts +++ b/src/triples/reader.ts @@ -14,6 +14,8 @@ * limitations under the License. */ import https from 'https'; +import fs from 'fs'; +import readline from 'readline'; import {Observable, Subscriber, TeardownLogic} from 'rxjs'; import {Log} from '../logging'; @@ -75,6 +77,44 @@ export function load(url: string): Observable { }); } +/** + * does the same as load(), but for a local file + */ +export function loadFile(path: string): Observable { + return new Observable(subscriber => { + handleFile(path, subscriber); + }); +} + +function handleFile( + path: string, + subscriber: Subscriber +): TeardownLogic { + const rl = readline.createInterface({ + input: fs.createReadStream(path), + crlfDelay: Infinity, + }); + + const data: string[] = []; + + rl.on('line', (line: string) => { + data.push(line); + }); + + rl.on('close', () => { + try { + const triples = toTripleStrings(data); + for (const triple of process(triples)) { + subscriber.next(triple); + } + } catch (error) { + Log(`Caught Error on end: ${error}`); + subscriber.error(error); + } + subscriber.complete(); + }); +} + function handleUrl(url: string, subscriber: Subscriber): TeardownLogic { https .get(url, response => { diff --git a/test/cli/args_logmessages_test.ts b/test/cli/args_logmessages_test.ts new file mode 100644 index 0000000..bbbe349 --- /dev/null +++ b/test/cli/args_logmessages_test.ts @@ -0,0 +1,49 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import {Readable} from 'stream'; +import fs from 'fs'; +import {main} from '../../src/cli/internal/main'; +import * as Logging from '../../src/logging'; +import * as Transform from '../../src/transform/transform'; + +describe('main Args logs', () => { + let readStreamCreatorFn: jest.SpyInstance; + beforeEach(() => { + const mockFileLine = ` .\n`; + const mockedStream = Readable.from([mockFileLine]); + readStreamCreatorFn = jest + .spyOn(fs, 'createReadStream') + //@ts-ignore + .mockImplementation(path => mockedStream); + }); + it(`the path it is loading from`, async () => { + const logs = ['']; + // log messages get caught for checking assert: + jest + .spyOn(Logging, 'Log') + .mockImplementation((msg: string) => void logs.push(msg)); + // but doesn't write the output .ts-file: + jest + .spyOn(Transform, 'WriteDeclarations') + .mockImplementation(async (...args) => {}); + await main(['--file', `ontology-file.nt`, `--verbose`]); + expect(logs.join('')).toMatchInlineSnapshot( + `"Loading Ontology from path: ontology-file.nt"` + ); + }); +}); diff --git a/test/cli/args_test.ts b/test/cli/args_test.ts index 4d7630a..808718f 100644 --- a/test/cli/args_test.ts +++ b/test/cli/args_test.ts @@ -24,6 +24,7 @@ describe('ParseFlags', () => { expect(options.context).toBe('https://schema.org'); expect(options.deprecated).toBe(true); expect(options.verbose).toBe(false); + expect(options.file).toBeUndefined(); expect(options.ontology).toBe( 'https://schema.org/version/latest/schemaorg-current-https.nt' @@ -37,6 +38,13 @@ describe('ParseFlags', () => { expect(options.ontology).toBe('https://google.com/foo'); }); + it('custom file', () => { + const options = ParseFlags(['--file', './ontology.nt'])!; + expect(options).not.toBeUndefined(); + + expect(options.file).toBe('./ontology.nt'); + }); + describe('deprecated fields', () => { let mockExit: jest.MockInstance< ReturnType, diff --git a/test/triples/reader_test.ts b/test/triples/reader_test.ts index 92845f4..14cc8bf 100644 --- a/test/triples/reader_test.ts +++ b/test/triples/reader_test.ts @@ -17,9 +17,10 @@ import {ClientRequest, IncomingMessage} from 'http'; import https from 'https'; import {toArray} from 'rxjs/operators'; -import {PassThrough, Writable} from 'stream'; +import {PassThrough, Readable, Writable} from 'stream'; -import {load} from '../../src/triples/reader'; +import fs from 'fs'; +import {load, loadFile} from '../../src/triples/reader'; import {Triple} from '../../src/triples/triple'; import {SchemaString, UrlNode} from '../../src/triples/types'; import {flush} from '../helpers/async'; @@ -434,6 +435,38 @@ describe('load', () => { ]); }); }); + describe('local file', () => { + let readStreamCreatorFn: jest.SpyInstance; + beforeEach(() => { + const mockFileLine = ` .\n`; + const mockedStream = Readable.from([mockFileLine]); + readStreamCreatorFn = jest + .spyOn(fs, 'createReadStream') + //@ts-ignore + .mockImplementation(path => mockedStream); + }); + it('fails loading a file (containing .nt syntax errors)', async () => { + const failingMockPath = './bad-ontology.nt'; + const failingMockLine = ` failingMockedStream); + + const fileTriples = loadFile(failingMockPath).toPromise(); + await expect(fileTriples).rejects.toThrow('Unexpected'); + }); + it('loads a file from the correct path', async () => { + const mockFilePath = './ontology.nt'; + + const fileTriples = loadFile(mockFilePath).toPromise(); + + expect(readStreamCreatorFn).toBeCalledWith(mockFilePath); + await expect(fileTriples).resolves.toEqual({ + Subject: UrlNode.Parse('https://schema.org/Person'), + Predicate: UrlNode.Parse('https://schema.org/knowsAbout'), + Object: UrlNode.Parse('https://schema.org/Event')!, + }); + }); + }); }); });