From a8e78c29d78af1b10c2263e9e3ede63bb7c2b89e Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Mon, 8 Nov 2021 18:38:30 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Implement=20a=20new=20path=20parser?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/utils/InstanceEncoder.test.js | 20 ++-- frontend/src/utils/ParsePath.test.js | 45 +++---- frontend/src/utils/ParsePath.ts | 133 ++++++++++++++++----- 3 files changed, 140 insertions(+), 58 deletions(-) diff --git a/frontend/src/utils/InstanceEncoder.test.js b/frontend/src/utils/InstanceEncoder.test.js index 7af9dd2..7886d95 100644 --- a/frontend/src/utils/InstanceEncoder.test.js +++ b/frontend/src/utils/InstanceEncoder.test.js @@ -5,17 +5,17 @@ import { InstanceEncoder } from "./InstanceEncoder" test("encodes pathless URL", () => { expect( - InstanceEncoder.encode(new URL("https://api.sophon.steffo.eu")), + InstanceEncoder.encode(new URL("https://api.sophon.steffo.eu/")), ).toEqual( - "https:api.sophon.steffo.eu", + "https:api.sophon.steffo.eu:", ) }) test("encodes URL with port number", () => { expect( - InstanceEncoder.encode(new URL("http://localhost:30033")), + InstanceEncoder.encode(new URL("http://localhost:30033/")), ).toEqual( - "http:localhost%3A30033", + "http:localhost%3A30033:", ) }) @@ -43,23 +43,23 @@ test("does not encode URL with %3A in path", () => { test("decodes pathless URL", () => { expect( - InstanceEncoder.decode("https:api.sophon.steffo.eu"), + InstanceEncoder.decode("https:api.sophon.steffo.eu:").toString(), ).toEqual( - "https://api.sophon.steffo.eu", + "https://api.sophon.steffo.eu/", ) }) test("decodes URL with port number", () => { expect( - InstanceEncoder.decode("http:localhost%3A30033"), + InstanceEncoder.decode("http:localhost%3A30033:").toString(), ).toEqual( - "http://localhost:30033", + "http://localhost:30033/", ) }) test("decodes URL with simple path", () => { expect( - InstanceEncoder.decode("https:steffo.eu:sophon:api:"), + InstanceEncoder.decode("https:steffo.eu:sophon:api:").toString(), ).toEqual( "https://steffo.eu/sophon/api/", ) @@ -67,7 +67,7 @@ test("decodes URL with simple path", () => { test("decodes URL with colon in path", () => { expect( - InstanceEncoder.decode("https:steffo.eu:sophon%3Aapi:"), + InstanceEncoder.decode("https:steffo.eu:sophon%3Aapi:").toString(), ).toEqual( "https://steffo.eu/sophon:api/", ) diff --git a/frontend/src/utils/ParsePath.test.js b/frontend/src/utils/ParsePath.test.js index 683bc9a..733f642 100644 --- a/frontend/src/utils/ParsePath.test.js +++ b/frontend/src/utils/ParsePath.test.js @@ -5,74 +5,79 @@ test("parses empty path", () => { expect( parsePath("/"), ).toMatchObject( - {}, + { + count: 0, + valid: true, + }, ) }) test("parses instance path", () => { expect( - parsePath("/i/https:api:sophon:steffo:eu:"), + parsePath("/i/https:api:sophon:steffo:eu:/"), ).toMatchObject( { instance: "https:api:sophon:steffo:eu:", + count: 1, + valid: true, }, ) }) -test("parses username path", () => { +test("parses logged in path", () => { expect( - parsePath("/i/https:api:sophon:steffo:eu:/u/steffo"), + parsePath("/i/https:api:sophon:steffo:eu:/l/logged-in/"), ).toMatchObject( { instance: "https:api:sophon:steffo:eu:", - userName: "steffo", - }, - ) -}) - -test("parses userid path", () => { - expect( - parsePath("/i/https:api:sophon:steffo:eu:/u/1"), - ).toMatchObject( - { - instance: "https:api:sophon:steffo:eu:", - userId: "1", + loggedIn: "logged-in", + count: 2, + valid: true, }, ) }) test("parses research group path", () => { expect( - parsePath("/i/https:api:sophon:steffo:eu:/g/testers"), + parsePath("/i/https:api:sophon:steffo:eu:/l/logged-in/g/testers/"), ).toMatchObject( { instance: "https:api:sophon:steffo:eu:", + loggedIn: "logged-in", researchGroup: "testers", + count: 3, + valid: true, }, ) }) test("parses research project path", () => { expect( - parsePath("/i/https:api:sophon:steffo:eu:/g/testers/p/test"), + parsePath("/i/https:api:sophon:steffo:eu:/l/logged-in/g/testers/p/test/"), ).toMatchObject( { instance: "https:api:sophon:steffo:eu:", + loggedIn: "logged-in", researchGroup: "testers", researchProject: "test", + count: 4, + valid: true, }, ) }) -test("parses research project path", () => { +test("parses notebook path", () => { expect( - parsePath("/i/https:api:sophon:steffo:eu:/g/testers/p/test/n/testerino"), + parsePath("/i/https:api:sophon:steffo:eu:/l/logged-in/g/testers/p/test/n/testerino/"), ).toMatchObject( { instance: "https:api:sophon:steffo:eu:", + loggedIn: "logged-in", researchGroup: "testers", researchProject: "test", notebook: "testerino", + count: 5, + valid: true, }, ) }) diff --git a/frontend/src/utils/ParsePath.ts b/frontend/src/utils/ParsePath.ts index 2d9d10b..4928484 100644 --- a/frontend/src/utils/ParsePath.ts +++ b/frontend/src/utils/ParsePath.ts @@ -25,44 +25,121 @@ export interface ParsedPath { /** * Passed the login page (either by browsing as guest or by logging in). */ - loggedIn?: boolean, + loggedIn?: string, /** * The number of pages that separate this to the website root. */ count: number, + + /** + * Whether the path is valid or not. + */ + valid: boolean, } + +const INSTANCE_REGEX = /^[/]i[/](?[^\\\n]+?)(?[/].*?)?$/ +const AUTHORIZATION_REGEX = /^[/]l[/](?logged-in)(?[/].*?)?$/ +const GROUP_REGEX = /^[/]g[/](?[^\\\n]+?)(?[/].*?)?$/ +const PROJECT_REGEX = /^[/]p[/](?[^\\\n]+?)(?[/].*?)?$/ +const NOTEBOOK_REGEX = /^[/]n[/](?[^\\\n]+?)(?[/].*?)?$/ + + +interface ParsePathSegmentConfig { + path: string, + parsed: ParsedPath, + + regex: RegExp, + key: keyof ParsedPath, + + next: ((path: string, parsed: ParsedPath) => ParsedPath)[], +} + + +function parsePathSegment({path, parsed, regex, key, next}: ParsePathSegmentConfig): ParsedPath { + // If the path is empty, return + if(!path) { + return parsed + } + + // Try matching the regex + const match = path.match(regex) + + // If the match fails, it means the path is invalid + if(!match || !match.groups) { + parsed.valid = Boolean(path) + return parsed + } + + // Unpack the groups + const {value, rest} = match.groups + parsed[key] = value as never // WHAT? + parsed.count += 1 + + const results = next.map((func) => { + return func(rest, parsed) + }).reduce((a, b) => { + return {...a, ...b} + }, {}) + + return { + ...parsed, + ...results, + } +} + + /** * Split the URL path into various components. * @param path - The path to split. */ export function parsePath(path: string): ParsedPath { - let result: ParsedPath = { - count: 0, - } - - result.instance = path.match(/[/]i[/]([^/]+)/)?.[1] - result.researchGroup = path.match(/[/]g[/]([A-Za-z0-9_-]+)/)?.[1] - result.researchProject = path.match(/[/]p[/]([A-Za-z0-9_-]+)/)?.[1] - result.notebook = path.match(/[/]n[/]([A-Za-z0-9_-]+)/)?.[1] - result.loggedIn = Boolean(path.match(/[/]l[/]logged-in/)) - - if(result.instance) { - result.count += 1 - } - if(result.researchGroup) { - result.count += 1 - } - if(result.researchProject) { - result.count += 1 - } - if(result.notebook) { - result.count += 1 - } - if(result.loggedIn) { - result.count += 1 - } - - return result + return parsePathSegment({ + path, + parsed: {count: 0, valid: true}, + regex: INSTANCE_REGEX, + key: "instance", + next: [ + (path, parsed) => { + return parsePathSegment({ + path, + parsed, + regex: AUTHORIZATION_REGEX, + key: "loggedIn", + next: [ + (path, parsed) => { + return parsePathSegment({ + path, + parsed, + regex: GROUP_REGEX, + key: "researchGroup", + next: [ + (path, parsed) => { + return parsePathSegment({ + path, + parsed, + regex: PROJECT_REGEX, + key: "researchProject", + next: [ + (path, parsed) => { + return parsePathSegment({ + path, + parsed, + regex: NOTEBOOK_REGEX, + key: "notebook", + next: [], + }) + }, + ], + }) + }, + ], + }) + }, + ], + }) + }, + ] + }) }