generated from neoncitylights/typescript
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: move @neoncitylights/typed-http here
- Loading branch information
1 parent
2d616c7
commit ce892d6
Showing
20 changed files
with
16,960 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
/** | ||
* All types below are described by the informal JSON schema | ||
* defined by https://webconcepts.info/JSON-concepts | ||
*/ | ||
|
||
export type Concept = { | ||
/** | ||
* the concept’s name as it is referred to in the source data | ||
*/ | ||
concept: string, | ||
/** | ||
* the concept identifier (a URI) which can be used as a URI | ||
* in a browser, and is also used to identify the concept | ||
* in JSON data | ||
*/ | ||
id: URL, | ||
/** | ||
* the singular version of the concept’s human-readable name | ||
*/ | ||
'name-singular': string, | ||
/** | ||
* the plural version of the concept’s human-readable name | ||
*/ | ||
'name-plural': string, | ||
/** | ||
* identifies a registry of all well-known values, if such | ||
* a registry exists | ||
*/ | ||
registry?: URL, | ||
/** | ||
* an array of all known values for the concept | ||
*/ | ||
values: ConceptValue[], | ||
}; | ||
|
||
export type ConceptValue = { | ||
/** | ||
* The concept value itself | ||
*/ | ||
value: string, | ||
/** | ||
* The identifier of the concept that the value is defined for | ||
*/ | ||
concept: URL, | ||
/** | ||
* the value identifier (a URI) which can be used as a URI in | ||
* a browser, and is also used to identify the value in JSON | ||
* data. | ||
*/ | ||
id: URL, | ||
/** | ||
* An array of all known descriptions of the value | ||
*/ | ||
details: ConceptValueDetail[] | ||
}; | ||
|
||
export type ConceptValueDetail = { | ||
/** | ||
* A human-readable text snippet that describes the value | ||
*/ | ||
description: string, | ||
/** | ||
* a URI identifying the documentation where the concept | ||
* value is defined for | ||
*/ | ||
documentation: string, | ||
/** | ||
* the identifier of the specification from which the | ||
* definition and documentation have been harvested | ||
*/ | ||
specification: string, | ||
/** | ||
* a short human-readable name for the specification that | ||
* can be used when linking to the documentation | ||
*/ | ||
'spec-name': string, | ||
}; |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
export function makeDocTag(annotation: string, value: string): string { | ||
return `@${annotation} ${value}`; | ||
} | ||
|
||
export function makeDocSeeTag(label: string, link: URL): string { | ||
return makeDocTag('see', `[${label}](${link})`); | ||
} | ||
|
||
export function makeDocBlock(values: string[]): string { | ||
let docBlock = '/**\n'; | ||
for(const value of values) { | ||
docBlock += ` *${value === '' ? '\n' : ` ${value}\n`}`; | ||
} | ||
docBlock += ' */'; | ||
return docBlock; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,195 @@ | ||
import fs from 'fs'; | ||
import { | ||
capitalize, | ||
Concept, | ||
ConceptValue, | ||
getDocumentationLabel, | ||
getHttpMethodAsCamelCase, | ||
isForbiddenHttpRequestHeader, | ||
makeCamelCase, | ||
makeDocBlock, | ||
makeDocSeeTag, | ||
makeExcludeType, | ||
makeStringType, | ||
makeType, | ||
makeUnionType, | ||
READ_FILE_PATH, | ||
} from '.'; | ||
import wordWrap from 'word-wrap'; | ||
|
||
function createRunGenerator( | ||
writeFilePath: string, | ||
conceptName: string, | ||
endMessage: (concept: Concept) => string, | ||
generateFn: (concept: Concept, writeStream: fs.WriteStream) => void, | ||
) { | ||
console.time(conceptName); | ||
fs.readFile(READ_FILE_PATH, 'utf8', (err, data) => { | ||
if(err) { | ||
console.error(err); | ||
return; | ||
} | ||
|
||
const concepts: Concept[] = JSON.parse(data); | ||
const specificConcept = concepts.find((val) => val['concept'] === conceptName) as Concept; | ||
fs.truncateSync(writeFilePath, 0); | ||
console.log(`${writeFilePath} emptied`); | ||
|
||
const writeStream: fs.WriteStream = fs.createWriteStream(writeFilePath, {flags:'a'}); | ||
writeStream.write(makeDocBlock([ | ||
'This file is generated by the `build/generateHttpTypes.ts` script.', | ||
'To run it, run `npm run generateHttpTypes`.', | ||
'Do NOT edit this file directly.', | ||
]) + '\n\n'); | ||
|
||
generateFn(specificConcept, writeStream); | ||
console.timeEnd(conceptName); | ||
console.log(`${writeFilePath}: ${endMessage(specificConcept)}\n`); | ||
}); | ||
} | ||
|
||
function makeFullDocBlock(conceptValue: ConceptValue): string { | ||
const description = wordWrap(conceptValue.details[0].description, { width: 60, indent: '' }); | ||
const lines = description.split('\n'); | ||
|
||
const docLinks: string[] = []; | ||
const specLinks: string[] = []; | ||
|
||
for(const detail of conceptValue.details) { | ||
const docLabel = getDocumentationLabel(new URL(detail.documentation)); | ||
|
||
docLinks.push(makeDocSeeTag(`Documentation${docLabel}`, new URL(detail.documentation))); | ||
specLinks.push(makeDocSeeTag(`Specification → ${detail['spec-name']}`, | ||
new URL(detail.specification))); | ||
} | ||
|
||
const docBlock = makeDocBlock([ | ||
...lines.map((line) => line.trim()), | ||
'', | ||
...docLinks, | ||
...specLinks, | ||
]); | ||
|
||
return docBlock; | ||
} | ||
|
||
// Generate HTTP methods | ||
createRunGenerator( | ||
'./src/httpMethods.ts', | ||
'http-method', | ||
(concepts) => `Exported ${concepts.values.length} HTTP methods`, | ||
(concept, writeStream) => { | ||
const httpMethodTypes: string[] = []; | ||
|
||
// generate each individual HTTP method type | ||
for(const conceptValue of concept.values) { | ||
const httpMethodType = `HttpMethod${getHttpMethodAsCamelCase(conceptValue.value)}`; | ||
httpMethodTypes.push(httpMethodType); | ||
|
||
const httpMethodName = conceptValue.value; | ||
const tsType = makeStringType( httpMethodType, httpMethodName); | ||
|
||
const docBlock = makeFullDocBlock(conceptValue); | ||
writeStream.write(`${docBlock}\n${tsType}\n\n`); | ||
} | ||
|
||
writeStream.write(makeUnionType('HttpMethod', httpMethodTypes) + '\n'); | ||
writeStream.end(); | ||
}, | ||
); | ||
|
||
// Generate HTTP status codes | ||
createRunGenerator( | ||
'./src/httpStatusCodes.ts', | ||
'http-status-code', | ||
(concepts) => `Exported ${concepts.values.length} HTTP status codes`, | ||
(concept, writeStream) => { | ||
const httpStatusCodeTypes: string[] = []; | ||
|
||
// categories | ||
const infoStatusCodes: string[] = []; | ||
const successStatusCodes: string[] = []; | ||
const redirectStatusCodes: string[] = []; | ||
const clientErrorStatusCodes: string[] = []; | ||
const serverErrorStatusCodes: string[] = []; | ||
|
||
// generate each individual HTTP status code type | ||
for(const conceptValue of concept.values) { | ||
const httpStatusCodeType = `HttpStatusCode${capitalize(makeCamelCase(conceptValue.value))}`; | ||
httpStatusCodeTypes.push(httpStatusCodeType); | ||
|
||
const httpStatusCodeName = conceptValue.value; | ||
const tsType = makeType( httpStatusCodeType, httpStatusCodeName, false); | ||
|
||
const docBlock = makeFullDocBlock(conceptValue); | ||
writeStream.write(`${docBlock}\n${tsType}\n\n`); | ||
|
||
if(httpStatusCodeName.startsWith('1')) { | ||
infoStatusCodes.push(httpStatusCodeType); | ||
} | ||
else if(httpStatusCodeName.startsWith('2')) { | ||
successStatusCodes.push(httpStatusCodeType); | ||
} | ||
else if(httpStatusCodeName.startsWith('3')) { | ||
redirectStatusCodes.push(httpStatusCodeType); | ||
} | ||
else if(httpStatusCodeName.startsWith('4')) { | ||
clientErrorStatusCodes.push(httpStatusCodeType); | ||
} | ||
else if(httpStatusCodeName.startsWith('5')) { | ||
serverErrorStatusCodes.push(httpStatusCodeType); | ||
} | ||
} | ||
|
||
// generate union type of all HTTP status codes | ||
writeStream.write(makeUnionType('HttpInfoStatusCode', infoStatusCodes) + '\n\n'); | ||
writeStream.write(makeUnionType('HttpSuccessStatusCode', successStatusCodes) + '\n\n'); | ||
writeStream.write(makeUnionType('HttpRedirectStatusCode', redirectStatusCodes) + '\n\n'); | ||
writeStream.write(makeUnionType('HttpClientErrorStatusCode', clientErrorStatusCodes) + '\n\n'); | ||
writeStream.write(makeUnionType('HttpServerErrorStatusCode', serverErrorStatusCodes) + '\n\n'); | ||
writeStream.write(makeUnionType('HttpStatusCode', [ | ||
'HttpInfoStatusCode', | ||
'HttpSuccessStatusCode', | ||
'HttpRedirectStatusCode', | ||
'HttpClientErrorStatusCode', | ||
'HttpServerErrorStatusCode', | ||
]) + '\n'); | ||
|
||
writeStream.end(); | ||
}, | ||
); | ||
|
||
// Generate HTTP headers | ||
createRunGenerator( | ||
'./src/httpHeaders.ts', | ||
'http-header', | ||
(concept) => `Exported ${concept.values.length} HTTP headers`, | ||
(concept, writeStream) => { | ||
const httpHeaderTypes: string[] = []; | ||
const forbiddenHttpRequestHeaders: string[] = []; | ||
|
||
// generate each individual HTTP header type | ||
for(const conceptValue of concept.values) { | ||
const httpHeaderType = `HttpHeader${capitalize(makeCamelCase(conceptValue.value))}`; | ||
httpHeaderTypes.push(httpHeaderType); | ||
|
||
const httpHeaderName = capitalize(conceptValue.value); | ||
const tsType = makeStringType(httpHeaderType, httpHeaderName); | ||
if(isForbiddenHttpRequestHeader(httpHeaderName)) { | ||
forbiddenHttpRequestHeaders.push(httpHeaderType); | ||
} | ||
|
||
const docBlock = makeFullDocBlock(conceptValue); | ||
writeStream.write(`${docBlock}\n${tsType}\n\n`); | ||
} | ||
|
||
// generate union type of all HTTP headers | ||
writeStream.write(makeUnionType('HttpHeader', httpHeaderTypes) + '\n\n'); | ||
writeStream.write(makeUnionType('ForbiddenHttpRequestHeader', forbiddenHttpRequestHeaders) + '\n\n'); | ||
writeStream.write(makeUnionType('ForbiddenHttpResponseHeader', [ 'HttpHeaderSetCookie', 'HttpHeaderSetCookie2' ]) + '\n\n'); | ||
writeStream.write(makeExcludeType('HttpRequestHeader', 'HttpHeader', 'ForbiddenHttpRequestHeader') + '\n\n'); | ||
writeStream.write(makeExcludeType('HttpResponseHeader', 'HttpHeader', 'ForbiddenHttpResponseHeader') + '\n'); | ||
|
||
writeStream.end(); | ||
}, | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export const READ_FILE_PATH = './build/concepts.json'; | ||
export * from './conceptTypes'; | ||
export * from './docUtils'; | ||
export * from './stringUtils'; | ||
export * from './typeUtils'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
export function makeCamelCase(str: string): string { | ||
const words = str.split('-'); | ||
return words[0] + words.slice(1).map((word) => word[0].toUpperCase() + word.slice(1)).join(''); | ||
} | ||
|
||
export function capitalize(str: string): string { | ||
return str[0].toUpperCase() + str.slice(1); | ||
} | ||
|
||
export function getDocumentationLabel(link: URL) { | ||
const path = link.pathname; | ||
|
||
switch(link.hostname) { | ||
case 'datatracker.ietf.org': { | ||
// example URLs: | ||
// - https://datatracker.ietf.org/doc/html/rfc7694#section-3 | ||
// - https://datatracker.ietf.org/doc/html/rfc7089#section-2.1.1 | ||
const regex = /(draft-(\w|-)+|rfc(\d{4,}))#section-(\d+)((.\d*)*)/; | ||
const ietfPath = path.substring('/doc/html/'.length) + link.hash; | ||
const matches = regex.exec(ietfPath); | ||
|
||
if(matches !== null) { | ||
const isDraft: boolean = matches[1].startsWith('draft'); | ||
const specType: string = isDraft ? 'Internet Draft' : 'IETF RFC'; | ||
const specName: string = isDraft ? matches[1].substring('draft-'.length) : matches[3]; | ||
const section: string = matches[4] + matches[5]; | ||
|
||
return ` → ${specType} ${specName} §${section}`; | ||
} | ||
} | ||
break; | ||
default: | ||
return ''; | ||
} | ||
} | ||
|
||
export function getHttpMethodAsCamelCase(method: string): string { | ||
switch(method) { | ||
case 'MKACTIVITY': return 'MkActivity'; | ||
case 'MKCALENDAR': return 'MkCalendar'; | ||
case 'MKCOL': return 'MkCol'; | ||
case 'MKREDIRECTREF': return 'MkRedirectRef'; | ||
case 'MKWORKSPACE': return 'MkWorkspace'; | ||
case 'ORDERPATCH': return 'OrderPatch'; | ||
case 'PROPFIND': return 'PropFind'; | ||
case 'PROPPATCH': return 'PropPatch'; | ||
case 'UPDATEREDIRECTREF': return 'UpdateRedirectRef'; | ||
default: return capitalize(makeCamelCase(method.toLowerCase())); | ||
} | ||
} | ||
|
||
export function isForbiddenHttpRequestHeader(header: string): boolean { | ||
return header.startsWith('Proxy') | ||
|| header.startsWith('Sec') | ||
|| [ | ||
'Accept-Charset', | ||
'Accept-Encoding', | ||
'Access-Control-Request-Headers', | ||
'Access-Control-Request-Method', | ||
'Connection', | ||
'Content-Length', | ||
'Cookie', | ||
'Date', | ||
'DNT', | ||
'Expect', | ||
'Feature-Policy', | ||
'Host', | ||
'Keep-Alive', | ||
'Origin', | ||
'Referer', | ||
'TE', | ||
'Trailer', | ||
'Transfer-Encoding', | ||
'Upgrade', | ||
'Via', | ||
].includes(header); | ||
} |
Oops, something went wrong.