Skip to content

Commit

Permalink
🔧 Refactor myst-frontmatter (#734)
Browse files Browse the repository at this point in the history
* 💣 Pull apart giant frontmatter file

* 🧪 Relocate frontmatter tests closer to code

* 🔧Share frontmatter aliases, improve validation naming

* 💣 Split tests out from giant files into smaller files

* 🕵️‍♀️ Remove aliases that do not resolve to valid key

* 📮 Postal code may be a number in frontmatter

* 🔧 Tweak how thebe is coerced when invalid for idempotency

* 🔧 Move jupytext/kernelspec frontmatter code

* 🔧 Move list coercing to simple-validators

* 🔧 Add missing exports

* 🧹 Clean up commented code
  • Loading branch information
fwkoch authored Nov 7, 2023
1 parent 4534f99 commit f15ec37
Show file tree
Hide file tree
Showing 81 changed files with 3,612 additions and 3,053 deletions.
5 changes: 5 additions & 0 deletions .changeset/large-laws-admire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'myst-frontmatter': patch
---

Postal code may be a number
6 changes: 6 additions & 0 deletions .changeset/soft-fishes-applaud.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'simple-validators': patch
'myst-frontmatter': patch
---

Move list coercing to simple-validators
5 changes: 5 additions & 0 deletions .changeset/spotty-dragons-move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'simple-validators': patch
---

Remove aliases that do not resolve to valid keys
6 changes: 6 additions & 0 deletions .changeset/strange-kids-tap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'myst-frontmatter': patch
'myst-config': patch
---

Share frontmatter aliases, improve validation naming
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 7 additions & 8 deletions packages/myst-config/src/project/validators.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { PROJECT_FRONTMATTER_KEYS, validateProjectFrontmatterKeys } from 'myst-frontmatter';
import {
FRONTMATTER_ALIASES,
PROJECT_FRONTMATTER_KEYS,
validateProjectFrontmatterKeys,
} from 'myst-frontmatter';
import type { ValidationOptions } from 'simple-validators';
import {
defined,
Expand All @@ -10,13 +14,8 @@ import {
import type { ProjectConfig } from './types.js';

const PROJECT_CONFIG_KEYS = {
optional: ['remote', 'index', 'exclude', 'plugins'].concat(PROJECT_FRONTMATTER_KEYS),
alias: {
jupyter: 'thebe',
author: 'authors',
affiliation: 'affiliations',
export: 'exports',
},
optional: ['remote', 'index', 'exclude', 'plugins', ...PROJECT_FRONTMATTER_KEYS],
alias: FRONTMATTER_ALIASES,
};

function validateProjectConfigKeys(value: Record<string, any>, opts: ValidationOptions) {
Expand Down
7 changes: 0 additions & 7 deletions packages/myst-config/src/site/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { Export, ProjectFrontmatter, SiteFrontmatter } from 'myst-frontmatter';
import { SITE_FRONTMATTER_KEYS } from 'myst-frontmatter';

export interface SiteProject {
slug?: string;
Expand All @@ -23,12 +22,6 @@ export interface SiteAction {
static?: boolean;
}

export const SITE_CONFIG_KEYS = {
optional: ['projects', 'nav', 'actions', 'domains', 'favicon', 'template'].concat(
SITE_FRONTMATTER_KEYS,
),
};

export type SiteTemplateOptions = Record<string, any>;

export type SiteConfig = SiteFrontmatter & {
Expand Down
20 changes: 18 additions & 2 deletions packages/myst-config/src/site/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ import {
validateBoolean,
validationError,
} from 'simple-validators';
import { validateSiteFrontmatterKeys } from 'myst-frontmatter';
import { SITE_CONFIG_KEYS } from './types.js';
import {
FRONTMATTER_ALIASES,
SITE_FRONTMATTER_KEYS,
validateSiteFrontmatterKeys,
} from 'myst-frontmatter';
import type {
SiteAction,
SiteConfig,
Expand All @@ -22,6 +25,19 @@ import type {
SiteTemplateOptions,
} from './types.js';

export const SITE_CONFIG_KEYS = {
optional: [
...SITE_FRONTMATTER_KEYS,
'projects',
'nav',
'actions',
'domains',
'favicon',
'template',
],
alias: FRONTMATTER_ALIASES,
};

function validateUrlOrPath(input: any, opts: ValidationOptions) {
const value = validateString(input, opts);
if (!defined(value)) return undefined;
Expand Down
1 change: 1 addition & 0 deletions packages/myst-frontmatter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
},
"devDependencies": {
"@types/spdx-correct": "^3.1.0",
"glob": "^10.3.1",
"js-yaml": "^4.1.0",
"moment": "^2.29.4"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -329,3 +329,74 @@ cases:
- id: univa
name: University A
errors: 1

- title: extra keys removed
raw:
affiliations:
- name: University A
id: univa
extra: ''
normalized:
affiliations:
- name: University A
id: univa
warnings: 1
- title: full object returns self
raw:
affiliations:
- id: abc123
address: 123 Example St.
city: Example town
state: EX
postal_code: '00000'
country: USA
name: Example University
department: Example department
collaboration: true
isni: '0000000000000000'
ringgold: 99999
ror: '0000000000000000'
url: http://example.com
email: [email protected]
phone: 1-800-000-0000
fax: (800) 000-0001
normalized:
affiliations:
- id: abc123
address: 123 Example St.
city: Example town
state: EX
postal_code: '00000'
country: USA
name: Example University
department: Example department
collaboration: true
isni: '0000000000000000'
ringgold: 99999
ror: '0000000000000000'
url: http://example.com
email: [email protected]
phone: 1-800-000-0000
fax: (800) 000-0001
- title: invalid ringgold number errors
raw:
affiliations:
- name: University A
id: univa
ringgold: 1
normalized:
affiliations:
- name: University A
id: univa
errors: 1
- title: numeric postal code coerces to string
raw:
affiliations:
- name: University A
id: univa
postal_code: 91104
normalized:
affiliations:
- name: University A
id: univa
postal_code: '91104'
20 changes: 20 additions & 0 deletions packages/myst-frontmatter/src/affiliations/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export interface Affiliation {
id?: string;
name?: string; // by default required but if only institution is provided, it's ok
institution?: string;
department?: string;
address?: string;
city?: string;
state?: string; // or region or province
postal_code?: string;
country?: string;
collaboration?: boolean;
isni?: string;
ringgold?: number;
ror?: string;
doi?: string;
url?: string;
email?: string;
phone?: string;
fax?: string;
}
131 changes: 131 additions & 0 deletions packages/myst-frontmatter/src/affiliations/validators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import type { ValidationOptions } from 'simple-validators';
import {
defined,
incrementOptions,
validateBoolean,
validateEmail,
validateNumber,
validateObjectKeys,
validateString,
validateUrl,
validationWarning,
} from 'simple-validators';
import { stashPlaceholder } from '../utils/referenceStash.js';
import { validateDoi } from '../utils/validators.js';
import type { Affiliation } from './types.js';

const AFFILIATION_KEYS = [
'id',
'address',
'city',
'state',
'postal_code',
'country',
'name',
'department',
'collaboration',
'isni',
'ringgold',
'ror',
'doi',
'url',
'email',
'phone',
'fax',
];

const AFFILIATION_ALIASES = {
ref: 'id', // Used in QMD to reference an affiliation
region: 'state',
province: 'state',
zipcode: 'postal_code',
zip_code: 'postal_code',
website: 'url',
institution: 'name',
};

/**
* Validate Affiliation object against the schema
*/
export function validateAffiliation(input: any, opts: ValidationOptions) {
if (typeof input === 'string') {
input = stashPlaceholder(input);
}
const value = validateObjectKeys(
input,
{ optional: AFFILIATION_KEYS, alias: AFFILIATION_ALIASES },
opts,
);
if (value === undefined) return undefined;
const output: Affiliation = {};
if (defined(value.id)) {
output.id = validateString(value.id, incrementOptions('id', opts));
}
if (defined(value.name)) {
output.name = validateString(value.name, incrementOptions('name', opts));
}
if (defined(value.department)) {
output.department = validateString(value.department, incrementOptions('department', opts));
}
if (defined(value.address)) {
output.address = validateString(value.address, incrementOptions('address', opts));
}
if (defined(value.city)) {
output.city = validateString(value.city, incrementOptions('city', opts));
}
if (defined(value.state)) {
output.state = validateString(value.state, incrementOptions('state', opts));
}
if (defined(value.postal_code)) {
output.postal_code = validateString(value.postal_code, {
coerceNumber: true,
...incrementOptions('postal_code', opts),
});
}
if (defined(value.country)) {
output.country = validateString(value.country, incrementOptions('country', opts));
}
// Both ISNI and ROR validation should occur similar to orcid (maybe in that same lib?)
if (defined(value.isni)) {
output.isni = validateString(value.isni, incrementOptions('isni', opts));
}
if (defined(value.ror)) {
output.ror = validateString(value.ror, incrementOptions('ror', opts));
}
if (defined(value.ringgold)) {
output.ringgold = validateNumber(value.ringgold, {
min: 1000,
max: 999999,
...incrementOptions('ringgold', opts),
});
}
if (defined(value.doi)) {
output.doi = validateDoi(value.doi, incrementOptions('doi', opts));
}
if (defined(value.collaboration)) {
output.collaboration = validateBoolean(
value.collaboration,
incrementOptions('collaboration', opts),
);
}
if (defined(value.email)) {
output.email = validateEmail(value.email, incrementOptions('email', opts));
}
if (defined(value.url)) {
output.url = validateUrl(value.url, incrementOptions('url', opts));
}
if (defined(value.phone)) {
output.phone = validateString(value.phone, incrementOptions('phone', opts));
}
if (defined(value.fax)) {
output.fax = validateString(value.fax, incrementOptions('fax', opts));
}
// If affiliation only has an id, give it a matching name; this is equivalent to the case
// where a simple string is provided as an affiliation.
if (Object.keys(output).length === 1 && output.id) {
return stashPlaceholder(output.id);
} else if (!output.name) {
validationWarning('affiliation should include name/institution', opts);
}
return output;
}
27 changes: 27 additions & 0 deletions packages/myst-frontmatter/src/biblio/biblio.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
title: Biblio
cases:
- title: empty object returns self
raw:
biblio: {}
normalized:
biblio: {}
- title: extra keys removed
raw:
biblio:
extra: ''
normalized:
biblio: {}
warnings: 1
- title: full object returns self
raw:
biblio:
volume: test
issue: example
first_page: 1
last_page: 2
normalized:
biblio:
volume: test
issue: example
first_page: 1
last_page: 2
2 changes: 2 additions & 0 deletions packages/myst-frontmatter/src/biblio/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './types.js';
export * from './validators.js';
7 changes: 7 additions & 0 deletions packages/myst-frontmatter/src/biblio/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type Biblio = {
// https://docs.openalex.org/about-the-data/work#biblio
volume?: string | number; // sometimes you'll get fun values like "Spring" and "Inside cover."
issue?: string | number;
first_page?: string | number;
last_page?: string | number;
};
Loading

0 comments on commit f15ec37

Please sign in to comment.