From 1f3ea03b7f6050a74b46d54f286a18686a9e5b58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduard=20Lavu=C5=A1?= Date: Thu, 14 Jan 2021 11:26:09 +0100 Subject: [PATCH 1/3] fix: parseProfileId not returning version label --- CHANGELOG.md | 5 ++++- src/language/syntax/rules/document_id.ts | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25f263e..a4aec01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## [Unreleased] +### Fixed +* Document id `parseProfileId` not returning version label + ## [0.0.11] - 2021-01-19 ### Added @@ -100,7 +103,7 @@ * Documentation extraction from doc strings * Usecase result parsing as optional -[Unreleased]: https://github.com/superfaceai/parser/compare/v0.0.10...HEAD +[Unreleased]: https://github.com/superfaceai/parser/compare/v0.0.11...HEAD [0.0.11]: https://github.com/superfaceai/parser/compare/v0.0.10...v0.0.11 [0.0.10]: https://github.com/superfaceai/parser/compare/v0.0.9...v0.0.10 [0.0.9]: https://github.com/superfaceai/parser/compare/v0.0.8...v0.0.9 diff --git a/src/language/syntax/rules/document_id.ts b/src/language/syntax/rules/document_id.ts index 1711ca7..2017847 100644 --- a/src/language/syntax/rules/document_id.ts +++ b/src/language/syntax/rules/document_id.ts @@ -191,6 +191,7 @@ export type ParseProfileIdResult = major: number; minor: number; patch: number; + label?: string; }; } | { kind: 'error'; message: string }; @@ -214,6 +215,7 @@ export function parseProfileId(id: string): ParseProfileIdResult { major: baseResult.version.major, minor: baseResult.version.minor, patch: baseResult.version.patch, + label: baseResult.version.label, }; } From 50f071295f6a9c268f72ced887429d05c35bfb34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduard=20Lavu=C5=A1?= Date: Thu, 14 Jan 2021 13:25:58 +0100 Subject: [PATCH 2/3] feat: update document id parsing * better interfaces for map and profile ids * document id partial parsing function * more flexible version parsing --- CHANGELOG.md | 7 + src/common/document/interfaces.ts | 45 +++ .../document/parser.test.ts} | 98 +++-- src/common/document/parser.ts | 266 ++++++++++++++ src/common/index.ts | 3 + src/common/split.ts | 34 ++ src/index.ts | 1 + src/language/syntax/index.ts | 8 - src/language/syntax/rules/document_id.ts | 334 ------------------ src/language/syntax/rules/map/common.ts | 19 +- src/language/syntax/rules/profile/profile.ts | 21 +- 11 files changed, 456 insertions(+), 380 deletions(-) create mode 100644 src/common/document/interfaces.ts rename src/{language/syntax/rules/document_id.test.ts => common/document/parser.test.ts} (55%) create mode 100644 src/common/document/parser.ts create mode 100644 src/common/index.ts create mode 100644 src/common/split.ts delete mode 100644 src/language/syntax/rules/document_id.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a4aec01..61bd3b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ ## [Unreleased] +### Added +* Public `parseDocumentId` for partial parsing + +### Changed +* Interfaces for map and profile id +* Improved document id version parsing + ### Fixed * Document id `parseProfileId` not returning version label diff --git a/src/common/document/interfaces.ts b/src/common/document/interfaces.ts new file mode 100644 index 0000000..0260040 --- /dev/null +++ b/src/common/document/interfaces.ts @@ -0,0 +1,45 @@ +/** + * Structure representing semver version tag. + */ +export type DocumentVersion = { + major: number; + minor?: number; + /** Patch version cannot appear without minor version. */ + patch?: number; + /** Label can appear even without major and minor version. */ + label?: string; +}; +/** + * Generic structure that covers both partial profile and partial map ids. + */ +export type DocumentId = { + /** The optional leading scope before `/`. */ + scope?: string; + /** The middle portion between of the `[/]middle[@]`, split by `.`. */ + middle: string[]; + /** The optional trailing version after `@`. */ + version?: DocumentVersion; +}; + +/** Information encoded in the profile id string. */ +export type ProfileDocumentId = { + /** Scope of the profile, if any. */ + scope?: string; + /** Name of the profile. */ + name: string; + /** Version of the profile. */ + version: DocumentVersion; +}; + +/** Information encoded in the map id string. */ +export type MapDocumentId = { + scope?: string; + name: string; + provider: string; + variant?: string; + version: { + major: number; + minor?: number; + revision?: number; + }; +}; diff --git a/src/language/syntax/rules/document_id.test.ts b/src/common/document/parser.test.ts similarity index 55% rename from src/language/syntax/rules/document_id.test.ts rename to src/common/document/parser.test.ts index c9befc8..a1921de 100644 --- a/src/language/syntax/rules/document_id.test.ts +++ b/src/common/document/parser.test.ts @@ -1,12 +1,33 @@ -import { parseMapId, parseProfileId, parseVersion } from './document_id'; +import { + parseDocumentId, + parseMapId, + parseProfileId, + parseVersion, +} from './parser'; describe('document name parsing', () => { - it('parses minimal profile name', () => { + it('parses partial profile name', () => { const id = 'my-profile'; + expect(parseDocumentId(id)).toEqual({ + kind: 'parsed', + value: { + middle: ['my-profile'], + }, + }); + }); + + it('parses minimal profile name', () => { + const id = 'my_profile@17'; + expect(parseProfileId(id)).toEqual({ kind: 'parsed', - name: 'my-profile', + value: { + name: 'my_profile', + version: { + major: 17, + }, + }, }); }); @@ -15,40 +36,59 @@ describe('document name parsing', () => { expect(parseProfileId(id)).toEqual({ kind: 'parsed', - scope: 'my-sc0pe', - name: 'my-profile', - version: { - major: 1, - minor: 2, - patch: 34, + value: { + scope: 'my-sc0pe', + name: 'my-profile', + version: { + major: 1, + minor: 2, + patch: 34, + }, }, }); }); - it('parses minimal map name', () => { + it('parses partial map name', () => { const id = 'my-profile.x_prov1der'; + expect(parseDocumentId(id)).toEqual({ + kind: 'parsed', + value: { + middle: ['my-profile', 'x_prov1der'], + }, + }); + }); + + it('parses minimal map name', () => { + const id = 'prof.prov@32'; + expect(parseMapId(id)).toEqual({ kind: 'parsed', - name: 'my-profile', - provider: 'x_prov1der', + value: { + name: 'prof', + provider: 'prov', + version: { + major: 32, + }, + }, }); }); it('parses full map name', () => { - const id = 'our_scope/my-profile.x_prov1der.v4riant@1.2.34-rev567'; + const id = 'our_scope/my-profile.x_prov1der.v4riant@1.2-rev567'; expect(parseMapId(id)).toEqual({ kind: 'parsed', - scope: 'our_scope', - name: 'my-profile', - provider: 'x_prov1der', - variant: 'v4riant', - version: { - major: 1, - minor: 2, - patch: 34, - revision: 567, + value: { + scope: 'our_scope', + name: 'my-profile', + provider: 'x_prov1der', + variant: 'v4riant', + version: { + major: 1, + minor: 2, + revision: 567, + }, }, }); }); @@ -56,6 +96,10 @@ describe('document name parsing', () => { it('returns an error for uppercase', () => { const id = 'SCOPE/profile'; + expect(parseDocumentId(id)).toStrictEqual({ + kind: 'error', + message: 'scope is not a valid lowercase identifier', + }); expect(parseProfileId(id)).toStrictEqual({ kind: 'error', message: 'scope is not a valid lowercase identifier', @@ -67,12 +111,12 @@ describe('document name parsing', () => { expect(parseMapId(id)).toStrictEqual({ kind: 'error', - message: 'provider is not a valid lowercase identifier', + message: '"" is not a valid lowercase identifier', }); expect(parseProfileId(id)).toStrictEqual({ kind: 'error', - message: 'name is not a valid lowercase identifier', + message: '"" is not a valid lowercase identifier', }); }); @@ -81,12 +125,12 @@ describe('document name parsing', () => { expect(parseMapId(id)).toStrictEqual({ kind: 'error', - message: 'variant is not a valid lowercase identifier', + message: '"" is not a valid lowercase identifier', }); expect(parseProfileId(id)).toStrictEqual({ kind: 'error', - message: 'name is not a valid lowercase identifier', + message: '"" is not a valid lowercase identifier', }); }); @@ -96,7 +140,7 @@ describe('document name parsing', () => { expect(parseMapId(id)).toStrictEqual({ kind: 'error', message: - 'could not parse revision: label must be in format `revN` where N is a non-negative integer', + 'revision label must be in format `revN` where N is a non-negative integer', }); }); diff --git a/src/common/document/parser.ts b/src/common/document/parser.ts new file mode 100644 index 0000000..b2a7b66 --- /dev/null +++ b/src/common/document/parser.ts @@ -0,0 +1,266 @@ +import { splitLimit } from '../split'; +import { + DocumentId, + DocumentVersion, + MapDocumentId, + ProfileDocumentId, +} from './interfaces'; + +export type ParseResult = + | { kind: 'parsed'; value: T } + | { kind: 'error'; message: string }; + +const ID_NAME_RE = /^[a-z][a-z0-9_-]*$/; +/** + * Checks whether the identififer is a lowercase identififer as required for document ids in the spec. + */ +export function isLowercaseIdentifier(str: string): boolean { + return ID_NAME_RE.test(str); +} + +const VERSION_NUMBER_RE = /^[0-9]+$/; +/** + * Parses a singular version number or returns undefined. + */ +function parseVersionNumber(str: string): number | undefined { + const value = str.trim(); + if (!VERSION_NUMBER_RE.test(value)) { + return undefined; + } + + return parseInt(value, 10); +} + +/** + * Parses version in format `major.minor.patch-label` + */ +export function parseVersion(version: string): ParseResult { + const [restVersion, label] = splitLimit(version, '-', 1); + const [majorStr, minorStr, patchStr] = splitLimit(restVersion, '.', 2); + + const major = parseVersionNumber(majorStr); + if (major === undefined) { + return { kind: 'error', message: 'major component is not a valid number' }; + } + + let minor = undefined; + if (minorStr !== undefined) { + minor = parseVersionNumber(minorStr); + if (minor === undefined) { + return { + kind: 'error', + message: 'minor component is not a valid number', + }; + } + } + + let patch = undefined; + if (patchStr !== undefined) { + patch = parseVersionNumber(patchStr); + if (patch === undefined) { + return { + kind: 'error', + message: 'patch component is not a valid number', + }; + } + } + + return { + kind: 'parsed', + value: { + major, + minor, + patch, + label, + }, + }; +} + +/** Parses document id. + * + * This parses a more general structure that fits both the profile and map id. + */ +export function parseDocumentId(id: string): ParseResult { + // parse scope first + let scope: string | undefined; + const [splitScope, scopeRestId] = splitLimit(id, '/', 1); + if (scopeRestId !== undefined) { + scope = splitScope; + if (!isLowercaseIdentifier(scope)) { + return { + kind: 'error', + message: 'scope is not a valid lowercase identifier', + }; + } + + // strip the scope + id = scopeRestId; + } + + let parsedVersion; + const [versionRestId, splitVersion] = splitLimit(id, '@', 1); + if (splitVersion !== undefined) { + parsedVersion = parseVersion(splitVersion); + + if (parsedVersion.kind === 'error') { + return { + kind: 'error', + message: 'could not parse version: ' + parsedVersion.message, + }; + } + + // strip the version + id = versionRestId; + } + + const middle = id.split('.'); + for (const m of middle) { + if (!isLowercaseIdentifier(m)) { + return { + kind: 'error', + message: `"${m}" is not a valid lowercase identifier`, + }; + } + } + + // unpack version + let version = undefined; + if (parsedVersion !== undefined) { + version = { + major: parsedVersion.value.major, + minor: parsedVersion.value.minor, + patch: parsedVersion.value.patch, + label: parsedVersion.value.label, + }; + } + + return { + kind: 'parsed', + value: { + scope, + middle, + version, + }, + }; +} + +/** Parses the id using `parseDocumentId`, checks that the `middle` is a valid `name`. */ +export function parseProfileId(id: string): ParseResult { + const baseResult = parseDocumentId(id); + if (baseResult.kind === 'error') { + return baseResult; + } + const base = baseResult.value; + + if (base.middle.length !== 1) { + return { + kind: 'error', + message: `"${base.middle.join('.')}" is not a valid lowercase identifier`, + }; + } + + if (base.version === undefined) { + return { + kind: 'error', + message: 'profile id requires a version tag', + }; + } + const version = { + major: base.version.major, + minor: base.version.minor, + patch: base.version.patch, + label: base.version.label, + }; + + return { + kind: 'parsed', + value: { + scope: base.scope, + name: base.middle[0], + version, + }, + }; +} + +/** + * Parses version label in format `revN` + */ +export function parseRevisionLabel(label: string): ParseResult { + let value = label.trim(); + + if (!value.startsWith('rev')) { + return { + kind: 'error', + message: 'revision label must be in format `revN`', + }; + } + value = value.slice(3); + + const revision = parseVersionNumber(value); + if (revision === undefined) { + return { + kind: 'error', + message: + 'revision label must be in format `revN` where N is a non-negative integer', + }; + } + + return { + kind: 'parsed', + value: revision, + }; +} + +/** + * Parses the id using `parseDocumentId`, checks that the middle portion contains + * a valid `name`, `porovider` and parses the revision tag, if any. + */ +export function parseMapId(id: string): ParseResult { + const baseResult = parseDocumentId(id); + if (baseResult.kind === 'error') { + return baseResult; + } + const base = baseResult.value; + + // parse name portion + const [name, provider, variant] = base.middle; + if (provider === undefined) { + return { + kind: 'error', + message: 'provider is not a valid lowercase identifier', + }; + } + + if (base.version === undefined) { + return { + kind: 'error', + message: 'version must be present in map id', + }; + } + let revision = undefined; + if (base.version.label !== undefined) { + const parseResult = parseRevisionLabel(base.version.label); + if (parseResult.kind === 'error') { + return parseResult; + } + + revision = parseResult.value; + } + + const version = { + major: base.version.major, + minor: base.version.minor, + revision, + }; + + return { + kind: 'parsed', + value: { + scope: base.scope, + name, + provider, + variant, + version, + }, + }; +} diff --git a/src/common/index.ts b/src/common/index.ts new file mode 100644 index 0000000..a6c8632 --- /dev/null +++ b/src/common/index.ts @@ -0,0 +1,3 @@ +export * from './document/interfaces'; +export * from './document/parser'; +export * from './split'; diff --git a/src/common/split.ts b/src/common/split.ts new file mode 100644 index 0000000..02fdbbb --- /dev/null +++ b/src/common/split.ts @@ -0,0 +1,34 @@ +/** + * Splits string at delimiter, stopping at maxSplits splits. + * + * The last element of the array contains the rest of the string. + * + * Example: + * ``` + * splitLimit('1.2.3.4', '.', 2) // ['1', '2', '3.4'] + * // Note that this is **not** the same as: + * str.split(delimiter, 3) // ['1', '2', '3'] + * ``` + */ +export function splitLimit( + str: string, + delimiter: string, + maxSplits: number +): string[] { + const result: string[] = []; + + let current = str; + while (result.length < maxSplits) { + const i = current.indexOf(delimiter); + if (i === -1) { + break; + } + + result.push(current.slice(0, i)); + current = current.slice(i + 1); + } + + result.push(current); + + return result; +} diff --git a/src/index.ts b/src/index.ts index 515a92a..c1baa82 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ export * from './errors'; export * from './language'; export * from './interpreter'; +export * from './common'; diff --git a/src/language/syntax/index.ts b/src/language/syntax/index.ts index ad2754d..8d65177 100644 --- a/src/language/syntax/index.ts +++ b/src/language/syntax/index.ts @@ -3,11 +3,3 @@ export * from './rule'; export * from './rules/profile'; export * from './rules/map'; export { PARSER_FEATURES } from './features'; -export { - parseMapId, - ParseMapIdResult, - parseProfileId, - ParseProfileIdResult, - parseVersion, - ParseVersionResult, -} from './rules/document_id'; diff --git a/src/language/syntax/rules/document_id.ts b/src/language/syntax/rules/document_id.ts deleted file mode 100644 index 2017847..0000000 --- a/src/language/syntax/rules/document_id.ts +++ /dev/null @@ -1,334 +0,0 @@ -const ID_NAME_RE = /^[a-z][a-z0-9_-]*$/; -export function isLowercaseIdentifier(str: string): boolean { - return ID_NAME_RE.test(str); -} - -/** - * Splits string at delimiter, stopping at maxSplits splits. - * - * The last element of the array contains the rest of the string. - * - * Example: - * ``` - * splitLimit('1.2.3.4', '.', 2) // ['1', '2', '3.4'] - * // Note that this is **not** the same as: - * str.split(delimiter, 3) // ['1', '2', '3'] - * ``` - */ -function splitLimit( - str: string, - delimiter: string, - maxSplits: number -): string[] { - const result: string[] = []; - - let current = str; - while (result.length < maxSplits) { - const i = current.indexOf(delimiter); - if (i === -1) { - break; - } - - result.push(current.slice(0, i)); - current = current.slice(i + 1); - } - - result.push(current); - - return result; -} - -export type ParseVersionResult = - | { - kind: 'parsed'; - major: number; - minor?: number; - patch?: number; - label?: string; - } - | { - kind: 'error'; - message: string; - }; -const VERSION_NUMBER_RE = /^[0-9]+$/; -function parseVersionNumber(str: string): number | undefined { - const value = str.trim(); - if (!VERSION_NUMBER_RE.test(value)) { - return undefined; - } - - return parseInt(value, 10); -} -/** - * Parses version in format `major.minor.patch-label` - */ -export function parseVersion(version: string): ParseVersionResult { - const [majorStr, minorStr, restStr] = splitLimit(version, '.', 2); - - let patchStr: string | undefined = undefined; - let label: string | undefined = undefined; - if (restStr !== undefined) { - [patchStr, label] = splitLimit(restStr, '-', 1); - } - - const major = parseVersionNumber(majorStr); - if (major === undefined) { - return { kind: 'error', message: 'major component is not a valid number' }; - } - - let minor = undefined; - if (minorStr !== undefined) { - minor = parseVersionNumber(minorStr); - if (minor === undefined) { - return { - kind: 'error', - message: 'minor component is not a valid number', - }; - } - } - - let patch = undefined; - if (patchStr !== undefined) { - patch = parseVersionNumber(patchStr); - if (patch === undefined) { - return { - kind: 'error', - message: 'patch component is not a valid number', - }; - } - } - - return { - kind: 'parsed', - major, - minor, - patch, - label, - }; -} - -/** - * Parses document name. - * - * This function parses both profile and map names and returns a corresponding result. - */ -export type ParseDocumentIdentifierResult = - | { - kind: 'parsed'; - scope?: string; - name: string; - version?: { - major: number; - minor: number; - patch: number; - label?: string; - }; - } - | { - kind: 'error'; - message: string; - }; -function parseDocumentId(id: string): ParseDocumentIdentifierResult { - // parse scope first - let scope: string | undefined; - const [splitScope, scopeRestId] = splitLimit(id, '/', 1); - if (scopeRestId !== undefined) { - scope = splitScope; - if (!isLowercaseIdentifier(scope)) { - return { - kind: 'error', - message: 'scope is not a valid lowercase identifier', - }; - } - - // strip the scope - id = scopeRestId; - } - - let parsedVersion; - const [versionRestId, splitVersion] = splitLimit(id, '@', 1); - if (splitVersion !== undefined) { - parsedVersion = parseVersion(splitVersion); - - if (parsedVersion.kind === 'error') { - return { - kind: 'error', - message: 'could not parse version: ' + parsedVersion.message, - }; - } - - // strip the version - id = versionRestId; - } - - const name = id; - - // unpack version - let version = undefined; - if (parsedVersion !== undefined) { - version = { - major: parsedVersion.major, - minor: parsedVersion.minor ?? 0, - patch: parsedVersion.patch ?? 0, - label: parsedVersion.label, - }; - } - - return { - kind: 'parsed', - scope, - name, - version, - }; -} - -export type ParseProfileIdResult = - | { - kind: 'parsed'; - scope?: string; - name: string; - version?: { - major: number; - minor: number; - patch: number; - label?: string; - }; - } - | { kind: 'error'; message: string }; -export function parseProfileId(id: string): ParseProfileIdResult { - const baseResult = parseDocumentId(id); - - if (baseResult.kind === 'error') { - return baseResult; - } - - if (!isLowercaseIdentifier(baseResult.name)) { - return { - kind: 'error', - message: 'name is not a valid lowercase identifier', - }; - } - - let version = undefined; - if (baseResult.version !== undefined) { - version = { - major: baseResult.version.major, - minor: baseResult.version.minor, - patch: baseResult.version.patch, - label: baseResult.version.label, - }; - } - - return { - kind: 'parsed', - scope: baseResult.scope, - name: baseResult.name, - version, - }; -} - -type ParseRevisionLabelResult = - | { kind: 'parsed'; revision: number } - | { kind: 'error'; message: string }; -/** - * Parses version label in format `revN` - */ -export function parseRevisionLabel(label: string): ParseRevisionLabelResult { - let value = label.trim(); - - if (!value.startsWith('rev')) { - return { kind: 'error', message: 'label must be in format `revN`' }; - } - value = value.slice(3); - - const revision = parseVersionNumber(value); - if (revision === undefined) { - return { - kind: 'error', - message: - 'label must be in format `revN` where N is a non-negative integer', - }; - } - - return { - kind: 'parsed', - revision, - }; -} - -export type ParseMapIdResult = - | { - kind: 'parsed'; - scope?: string; - name: string; - provider: string; - variant?: string; - version?: { - major: number; - minor: number; - patch: number; - revision?: number; - }; - } - | { kind: 'error'; message: string }; -export function parseMapId(id: string): ParseMapIdResult { - const baseResult = parseDocumentId(id); - - if (baseResult.kind === 'error') { - return baseResult; - } - - // parse name portion - const [name, provider, variant] = splitLimit(baseResult.name, '.', 2); - if (!isLowercaseIdentifier(name)) { - return { - kind: 'error', - message: 'name is not a valid lowercase identifier', - }; - } - if (provider !== undefined && !isLowercaseIdentifier(provider)) { - return { - kind: 'error', - message: 'provider is not a valid lowercase identifier', - }; - } - if (variant !== undefined && !isLowercaseIdentifier(variant)) { - return { - kind: 'error', - message: 'variant is not a valid lowercase identifier', - }; - } - - let version = undefined; - if (baseResult.version !== undefined) { - let revision = undefined; - if (baseResult.version.label !== undefined) { - const parsedRevision = parseRevisionLabel(baseResult.version.label); - - if (parsedRevision.kind === 'error') { - return { - kind: 'error', - message: 'could not parse revision: ' + parsedRevision.message, - }; - } - - revision = parsedRevision.revision; - } - - version = { - major: baseResult.version.major, - minor: baseResult.version.minor, - patch: baseResult.version.patch, - revision, - }; - } - - return { - kind: 'parsed', - scope: baseResult.scope, - name, - provider, - variant, - version, - }; -} diff --git a/src/language/syntax/rules/map/common.ts b/src/language/syntax/rules/map/common.ts index 9364999..d57e11c 100644 --- a/src/language/syntax/rules/map/common.ts +++ b/src/language/syntax/rules/map/common.ts @@ -10,6 +10,10 @@ import { StatementConditionNode, } from '@superfaceai/ast'; +import { + isLowercaseIdentifier, + parseProfileId, +} from '../../../../common/document/parser'; import { LexerTokenKind } from '../../../index'; import { JessieExpressionTerminationToken } from '../../../lexer/sublexer/jessie/expression'; import { IdentifierTokenData, StringTokenData } from '../../../lexer/token'; @@ -22,7 +26,6 @@ import { SyntaxRuleSeparator, } from '../../rule'; import { documentedNode, SrcNode, SyntaxRuleSrc } from '../common'; -import { isLowercaseIdentifier, parseProfileId } from '../document_id'; const TERMINATOR_LOOKAHEAD: Record< JessieExpressionTerminationToken, @@ -205,13 +208,14 @@ const PROFILE_ID = SyntaxRule.identifier('profile') .followedBy(SyntaxRuleSeparator.operator('=')) .andFollowedBy( SyntaxRule.string().andThen(id => { - const parsedId = parseProfileId(id.data.string); + const parseIdResult = parseProfileId(id.data.string); // must link to a profile - if (parsedId.kind !== 'parsed' || parsedId.version === undefined) { + if (parseIdResult.kind !== 'parsed') { return { kind: 'nomatch', }; } + const parsedId = parseIdResult.value; return { kind: 'match', @@ -223,7 +227,7 @@ const PROFILE_ID = SyntaxRule.identifier('profile') span: id.span, }, }; - }, 'profile id in format `[/]@` with lowecase identifiers') + }, 'profile id in format `[/]@` with lowercase identifiers') ) .map(([keyword, _op, id]) => { return { @@ -305,7 +309,12 @@ export const MAP_HEADER: SyntaxRuleSrc = documentedNode( profile: { scope: profile.scope, name: profile.name, - version: profile.version, + // TODO: should we default to zeros here? + version: { + major: profile.version.major, + minor: profile.version.minor ?? 0, + patch: profile.version.patch ?? 0, + }, }, provider: provider.provider, variant: maybeVariant?.variant, diff --git a/src/language/syntax/rules/profile/profile.ts b/src/language/syntax/rules/profile/profile.ts index 71b201a..32a9717 100644 --- a/src/language/syntax/rules/profile/profile.ts +++ b/src/language/syntax/rules/profile/profile.ts @@ -17,6 +17,10 @@ import { UseCaseSlotDefinitionNode, } from '@superfaceai/ast'; +import { + parseDocumentId, + parseVersion, +} from '../../../../common/document/parser'; import { IdentifierTokenData, LexerTokenKind } from '../../../lexer/token'; import { LexerTokenMatch, @@ -25,7 +29,6 @@ import { SyntaxRuleSeparator, } from '../../rule'; import { documentedNode, SrcNode, SyntaxRuleSrc } from '../common'; -import { parseProfileId, parseVersion } from '../document_id'; // MUTABLE RULES // @@ -452,19 +455,24 @@ const PROFILE_NAME = SyntaxRule.identifier('name') .followedBy(SyntaxRuleSeparator.operator('=')) .andFollowedBy( SyntaxRule.string().andThen(name => { - const parsedName = parseProfileId(name.data.string); + const parseNameResult = parseDocumentId(name.data.string); // profiles can't have version specified in the name - if (parsedName.kind !== 'parsed' || parsedName.version !== undefined) { + if ( + parseNameResult.kind !== 'parsed' || + parseNameResult.value.middle.length !== 1 || + parseNameResult.value.version !== undefined + ) { return { kind: 'nomatch', }; } + const parsedName = parseNameResult.value; return { kind: 'match', value: { scope: parsedName.scope, - name: parsedName.name, + name: parsedName.middle[0], location: name.location, span: name.span, }, @@ -486,10 +494,11 @@ const PROFILE_VERSION = SyntaxRule.identifier('version') .followedBy(SyntaxRuleSeparator.operator('=')) .andFollowedBy( SyntaxRule.string().andThen(version => { - const parsedVersion = parseVersion(version.data.string); - if (parsedVersion.kind !== 'parsed') { + const parseVersionResult = parseVersion(version.data.string); + if (parseVersionResult.kind !== 'parsed') { return { kind: 'nomatch' }; } + const parsedVersion = parseVersionResult.value; return { kind: 'match', From a60ec6541923b7454e7f90ea6d95c450f5ba24a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduard=20Lavu=C5=A1?= Date: Tue, 19 Jan 2021 15:15:31 +0100 Subject: [PATCH 3/3] chore: review fixes --- CHANGELOG.md | 1 + src/common/document/parser.ts | 28 ++++++------------------- src/language/syntax/rules/map/common.ts | 6 +++--- 3 files changed, 10 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61bd3b5..f9d329c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ ### Changed * Interfaces for map and profile id * Improved document id version parsing +* Rename `isLowercaseIdentifier` to `isValidDocumentIdentifier` ### Fixed * Document id `parseProfileId` not returning version label diff --git a/src/common/document/parser.ts b/src/common/document/parser.ts index b2a7b66..513704c 100644 --- a/src/common/document/parser.ts +++ b/src/common/document/parser.ts @@ -14,7 +14,7 @@ const ID_NAME_RE = /^[a-z][a-z0-9_-]*$/; /** * Checks whether the identififer is a lowercase identififer as required for document ids in the spec. */ -export function isLowercaseIdentifier(str: string): boolean { +export function isValidDocumentIdentifier(str: string): boolean { return ID_NAME_RE.test(str); } @@ -86,7 +86,7 @@ export function parseDocumentId(id: string): ParseResult { const [splitScope, scopeRestId] = splitLimit(id, '/', 1); if (scopeRestId !== undefined) { scope = splitScope; - if (!isLowercaseIdentifier(scope)) { + if (!isValidDocumentIdentifier(scope)) { return { kind: 'error', message: 'scope is not a valid lowercase identifier', @@ -112,10 +112,11 @@ export function parseDocumentId(id: string): ParseResult { // strip the version id = versionRestId; } + const version = parsedVersion?.value; const middle = id.split('.'); for (const m of middle) { - if (!isLowercaseIdentifier(m)) { + if (!isValidDocumentIdentifier(m)) { return { kind: 'error', message: `"${m}" is not a valid lowercase identifier`, @@ -123,17 +124,6 @@ export function parseDocumentId(id: string): ParseResult { } } - // unpack version - let version = undefined; - if (parsedVersion !== undefined) { - version = { - major: parsedVersion.value.major, - minor: parsedVersion.value.minor, - patch: parsedVersion.value.patch, - label: parsedVersion.value.label, - }; - } - return { kind: 'parsed', value: { @@ -165,19 +155,13 @@ export function parseProfileId(id: string): ParseResult { message: 'profile id requires a version tag', }; } - const version = { - major: base.version.major, - minor: base.version.minor, - patch: base.version.patch, - label: base.version.label, - }; return { kind: 'parsed', value: { scope: base.scope, name: base.middle[0], - version, + version: base.version, }, }; } @@ -213,7 +197,7 @@ export function parseRevisionLabel(label: string): ParseResult { /** * Parses the id using `parseDocumentId`, checks that the middle portion contains - * a valid `name`, `porovider` and parses the revision tag, if any. + * a valid `name`, `provider` and parses the revision tag, if any. */ export function parseMapId(id: string): ParseResult { const baseResult = parseDocumentId(id); diff --git a/src/language/syntax/rules/map/common.ts b/src/language/syntax/rules/map/common.ts index d57e11c..cada5ab 100644 --- a/src/language/syntax/rules/map/common.ts +++ b/src/language/syntax/rules/map/common.ts @@ -11,7 +11,7 @@ import { } from '@superfaceai/ast'; import { - isLowercaseIdentifier, + isValidDocumentIdentifier, parseProfileId, } from '../../../../common/document/parser'; import { LexerTokenKind } from '../../../index'; @@ -245,7 +245,7 @@ const PROVIDER_ID = SyntaxRule.identifier('provider') .followedBy(SyntaxRuleSeparator.operator('=')) .andFollowedBy( SyntaxRule.string().andThen(provider => { - if (!isLowercaseIdentifier(provider.data.string)) { + if (!isValidDocumentIdentifier(provider.data.string)) { return { kind: 'nomatch', }; @@ -276,7 +276,7 @@ export const MAP_VARIANT = SyntaxRule.identifier('variant') .followedBy(SyntaxRuleSeparator.operator('=')) .andFollowedBy( SyntaxRule.string().andThen(variant => { - if (!isLowercaseIdentifier(variant.data.string)) { + if (!isValidDocumentIdentifier(variant.data.string)) { return { kind: 'nomatch', };