feat(tools): introduce generic data API with versioning (#45989)

Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
Sem Bauke
2022-05-30 19:37:01 +02:00
committed by GitHub
parent 2b884d9dd9
commit 18920de10c
6 changed files with 201 additions and 126 deletions
+1
View File
@@ -205,6 +205,7 @@ curriculum/dist
curriculum/build
client/static/_redirects
client/static/mobile
client/static/curriculum-data
### UI Components ###
tools/ui-components/dist
@@ -1,8 +1,11 @@
const fs = require('fs');
const path = require('path');
import fs from 'fs';
import path from 'path';
const { getChallengesForLang } = require('../../../curriculum/getChallenges');
const { buildMobileCurriculum } = require('./build-mobile-curriculum');
import { getChallengesForLang } from '../../../curriculum/getChallenges';
import {
buildExtCurriculumData,
Curriculum
} from './build-external-curricula-data';
const { CURRICULUM_LOCALE } = process.env;
@@ -10,10 +13,10 @@ const globalConfigPath = path.resolve(__dirname, '../../../config');
// We are defaulting to English because the ids for the challenges are same
// across all languages.
getChallengesForLang('english')
.then(result => {
void getChallengesForLang('english')
.then((result: Record<string, unknown>) => {
if (CURRICULUM_LOCALE === 'english') {
buildMobileCurriculum(result);
buildExtCurriculumData('v1.0.0', result as Curriculum);
}
return result;
})
@@ -0,0 +1,118 @@
import { mkdirSync, writeFileSync, readFileSync } from 'fs';
import { resolve } from 'path';
import { SuperBlocks } from '../../../config/certification-settings';
type Intro = { [keyValue in SuperBlocks]: IntroProps };
export type Curriculum = { [keyValue in SuperBlocks]: CurriculumProps };
type SuperBlockKeys = keyof typeof SuperBlocks;
type SuperBlockValues = typeof SuperBlocks[SuperBlockKeys];
interface IntroProps extends CurriculumProps {
title: string;
intro: string[];
}
interface CurriculumProps {
blocks: Record<string, Block>;
}
interface Block {
desc: string[];
intro: string[];
challenges: Record<string, unknown>;
meta: Record<string, unknown>;
}
export const superBlockMobileAppOrder = {
'responsive-web-design': { public: true },
'2022/responsive-web-design': { public: false },
'javascript-algorithms-and-data-structures': { public: true },
'2022/javascript-algorithms-and-data-structures': { public: false },
'front-end-development-libraries': { public: false },
'data-visualization': { public: false },
'back-end-development-and-apis': { public: false },
'quality-assurance': { public: false },
'scientific-computing-with-python': { public: false },
'data-analysis-with-python': { public: false },
'information-security': { public: false },
'machine-learning-with-python': { public: false },
'coding-interview-prep': { public: false },
'relational-database': { public: false }
};
export function buildExtCurriculumData(
ver: string,
curriculum: Curriculum
): void {
const staticFolderPath = resolve(__dirname, '../../../client/static');
const versionPath = `${staticFolderPath}/curriculum-data/${ver}`;
const blockIntroPath = resolve(
__dirname,
'../../../client/i18n/locales/english/intro.json'
);
mkdirSync(versionPath, { recursive: true });
parseCurriculumData();
function parseCurriculumData() {
const superBlockKeys = Object.values(SuperBlocks);
writeToFile('availableSuperblocks', {
superblocks: [
superBlockMobileAppOrder,
Object.values(SuperBlocks).map(superblock =>
getSuperBlockName(superblock)
)
]
});
for (let i = 0; i < superBlockKeys.length; i++) {
const superBlock = <Curriculum>{};
const superBlockKey = Object.values(SuperBlocks)[i];
const blockNames = Object.keys(curriculum[superBlockKeys[i]].blocks);
if (blockNames.length === 0) continue;
superBlock[superBlockKey] = <CurriculumProps>{};
superBlock[superBlockKey]['blocks'] = {};
for (let j = 0; j < blockNames.length; j++) {
superBlock[superBlockKey]['blocks'][blockNames[j]] = <Block>{};
superBlock[superBlockKey]['blocks'][blockNames[j]]['desc'] =
getBlockDescription(superBlockKey, blockNames[j]);
superBlock[superBlockKey]['blocks'][blockNames[j]]['challenges'] =
curriculum[superBlockKey]['blocks'][blockNames[j]]['meta'];
}
writeToFile(superBlockKeys[i].replace(/\//, '-'), superBlock);
}
}
function writeToFile(fileName: string, data: Record<string, unknown>): void {
mkdirSync(versionPath, { recursive: true });
const filePath = `${versionPath}/${fileName}.json`;
writeFileSync(filePath, JSON.stringify(data, null, 2));
}
function getBlockDescription(
superBlockKeys: SuperBlockValues,
blockKey: string
): string[] {
const intros = JSON.parse(readFileSync(blockIntroPath, 'utf-8')) as Intro;
return intros[superBlockKeys]['blocks'][blockKey]['intro'];
}
function getSuperBlockName(superBlockKeys: SuperBlockValues): string {
const superBlocks = JSON.parse(
readFileSync(blockIntroPath, 'utf-8')
) as Intro;
return superBlocks[superBlockKeys].title;
}
}
@@ -1,68 +0,0 @@
const fs = require('fs');
const path = require('path');
exports.buildMobileCurriculum = function buildMobileCurriculum(json) {
const mobileStaticPath = path.resolve(__dirname, '../../../client/static');
const blockIntroPath = path.resolve(
__dirname,
'../../../client/i18n/locales/english/intro.json'
);
fs.mkdirSync(`${mobileStaticPath}/mobile`, { recursive: true });
writeAndParseCurriculumJson(json);
function writeAndParseCurriculumJson(curriculum) {
const superBlockKeys = Object.keys(curriculum).filter(
key => key !== 'certifications'
);
writeToFile('availableSuperblocks', {
// removing "/" as it will create an extra sub-path when accessed via an endpoint
superblocks: [
superBlockKeys.map(key => key.replace(/\//, '-')),
getSuperBlockNames(superBlockKeys)
]
});
for (let i = 0; i < superBlockKeys.length; i++) {
const superBlock = {};
const blockNames = Object.keys(curriculum[superBlockKeys[i]].blocks);
if (blockNames.length === 0) continue;
superBlock[superBlockKeys[i]] = {};
superBlock[superBlockKeys[i]]['blocks'] = {};
for (let j = 0; j < blockNames.length; j++) {
superBlock[superBlockKeys[i]]['blocks'][blockNames[j]] = {};
superBlock[superBlockKeys[i]]['blocks'][blockNames[j]]['desc'] =
getBlockDescription(superBlockKeys[i], blockNames[j]);
superBlock[superBlockKeys[i]]['blocks'][blockNames[j]]['challenges'] =
curriculum[superBlockKeys[i]]['blocks'][blockNames[j]]['meta'];
}
writeToFile(superBlockKeys[i].replace(/\//, '-'), superBlock);
}
}
function getBlockDescription(superBlockKey, blockKey) {
const intros = JSON.parse(fs.readFileSync(blockIntroPath));
return intros[superBlockKey]['blocks'][blockKey]['intro'];
}
function getSuperBlockNames(superBlockKeys) {
const superBlocks = JSON.parse(fs.readFileSync(blockIntroPath));
return superBlockKeys.map(key => superBlocks[key].title);
}
function writeToFile(fileName, json) {
const fullPath = `${mobileStaticPath}/mobile/${fileName}.json`;
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
fs.writeFileSync(fullPath, JSON.stringify(json, null, 2));
}
};
@@ -1,51 +0,0 @@
const path = require('path');
const fs = require('fs');
const { AssertionError } = require('chai');
const envData = require('../../../config/env.json');
const { mobileSchemaValidator } = require('./mobileSchema');
if (envData.clientLocale == 'english' && !envData.showUpcomingChanges) {
describe('mobile curriculum build', () => {
const mobileStaticPath = path.resolve(__dirname, '../../../client/static');
const blockIntroPath = path.resolve(
__dirname,
'../../../client/i18n/locales/english/intro.json'
);
const validateMobileSuperBlock = mobileSchemaValidator();
test('the mobile curriculum should have a static folder with multiple files', () => {
expect(fs.existsSync(`${mobileStaticPath}/mobile`)).toBe(true);
expect(
fs.readdirSync(`${mobileStaticPath}/mobile`).length
).toBeGreaterThan(0);
});
test('the mobile curriculum should have access to the intro.json file', () => {
expect(fs.existsSync(blockIntroPath)).toBe(true);
});
test('the files generated should have the correct schema', () => {
const fileArray = fs.readdirSync(`${mobileStaticPath}/mobile`);
fileArray
.filter(fileInArray => fileInArray !== 'availableSuperblocks.json')
.forEach(fileInArray => {
const fileContent = fs.readFileSync(
`${mobileStaticPath}/mobile/${fileInArray}`
);
const result = validateMobileSuperBlock(JSON.parse(fileContent));
if (result.error) {
throw new AssertionError(result.error, `file: ${fileInArray}`);
}
});
});
});
} else {
describe.skip('Mobile curriculum is not localized', () => {
test.todo('localized tests');
});
}
@@ -0,0 +1,72 @@
import path from 'path';
import fs from 'fs';
import { AssertionError } from 'chai';
import envData from '../../../config/env.json';
import { SuperBlocks } from '../../../config/certification-settings';
import { mobileSchemaValidator } from './mobileSchema';
import { superBlockMobileAppOrder } from './build-external-curricula-data';
if (envData.clientLocale == 'english' && !envData.showUpcomingChanges) {
const VERSION = 'v1.0.0';
describe('mobile curriculum build', () => {
const mobileStaticPath = path.resolve(__dirname, '../../../client/static');
const blockIntroPath = path.resolve(
__dirname,
'../../../client/i18n/locales/english/intro.json'
);
const validateMobileSuperBlock = mobileSchemaValidator();
test('the mobile curriculum should have a static folder with multiple files', () => {
expect(
fs.existsSync(`${mobileStaticPath}/curriculum-data/${VERSION}`)
).toBe(true);
expect(
fs.readdirSync(`${mobileStaticPath}/curriculum-data/${VERSION}`).length
).toBeGreaterThan(0);
});
test('the mobile curriculum should have access to the intro.json file', () => {
expect(fs.existsSync(blockIntroPath)).toBe(true);
});
test('the files generated should have the correct schema', () => {
const fileArray = fs.readdirSync(
`${mobileStaticPath}/curriculum-data/${VERSION}`
);
fileArray
.filter(fileInArray => fileInArray !== 'availableSuperblocks.json')
.forEach(fileInArray => {
const fileContent = fs.readFileSync(
`${mobileStaticPath}/curriculum-data/${VERSION}/${fileInArray}`,
'utf-8'
);
const result = validateMobileSuperBlock(JSON.parse(fileContent));
if (result.error) {
throw new AssertionError(
result.error.toString(),
`file: ${fileInArray}`
);
}
});
});
test('All SuperBlocks should be present in the mobile SuperBlock object', () => {
expect(Object.keys(superBlockMobileAppOrder)).toEqual(
expect.arrayContaining(Object.values(SuperBlocks))
);
expect(Object.keys(superBlockMobileAppOrder)).toHaveLength(
Object.values(SuperBlocks).length
);
});
});
} else {
describe.skip('Mobile curriculum is not localized', () => {
test.todo('localized tests');
});
}