fit-file-parser
Version:
Parse your .FIT files easily, directly from JS (Garmin, Polar, Suunto)
259 lines (258 loc) • 11.6 kB
JavaScript
import ts from 'typescript';
import { FIT } from './fit.js';
export function line() {
return ts.factory.createIdentifier('\n');
}
export function comment(contents) {
return [
ts.factory.createIdentifier('\n'),
ts.factory.createIdentifier(`// ${contents}`),
ts.factory.createIdentifier('\n'),
];
}
export function header() {
return ts.factory.createNodeArray([ts.factory.createJSDocComment(`
this file is auto generated using src/type_generator.ts
it parses the big FIT definition object from src/fit.js into usable typescript types
do not edit this file directly, instead edit the generator and
regenerate it with "npm run codegen"
`)])[0];
}
export function capitalize(s) {
if (s.length === 0) {
return s;
}
return (s[0].toUpperCase() + s.slice(1));
}
export function snakeToCamel(s) {
return s.split('_').map(part => capitalize(part)).join('');
}
export function unicodeToChar(text) {
return text.replace(/\\u[\dA-F]{4}/gi, (match) => {
return String.fromCharCode(Number.parseInt(match.replace(/\\u/g, ''), 16));
});
}
export function generateProperty(name, type, optional = true) {
return ts.factory.createPropertySignature(undefined, ts.factory.createIdentifier(name), optional ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) : undefined, type);
}
export function generateArrayProperty(name, type, optional = true) {
return generateProperty(name, ts.factory.createArrayTypeNode(type), optional);
}
export function generateTypeFromField(def) {
switch (def.type) {
case 'uint32_array':
case 'uint16_array':
case 'uint8_array':
case 'sint32_array':
case 'sint16_array':
case 'sint8_array':
case 'byte_array':
return ts.factory.createArrayTypeNode(ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword));
case 'bool':
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword);
case 'date_time':
case 'string':
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
case 'uint32':
case 'uint64':
case 'uint16':
case 'uint8':
case 'int32':
case 'int64':
case 'int16':
case 'int8':
case 'sint32':
case 'sint16':
case 'sint8':
case 'float32':
case 'float64':
case 'uint32z':
case 'uint64z':
case 'uint16z':
case 'uint8z':
case 'localtime_into_day':
case 'byte':
case 'device_index':
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword);
default:
return ts.factory.createTypeReferenceNode(snakeToCamel(def.type));
}
}
export function generateTypes(types) {
const nodes = [];
const typeNames = Object.keys(types);
typeNames.forEach((name) => {
if (name === 'message_index') {
return;
}
const type = types[name];
const names = Object.values(type);
const typeAlias = ts.factory.createTypeAliasDeclaration([
ts.factory.createModifier(ts.SyntaxKind.ExportKeyword),
], snakeToCamel(name), undefined, ts.factory.createUnionTypeNode([...names.map(n => ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(String(n)))), ...(name === 'mesg_num'
? [
ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral('definition')),
]
: [])]));
nodes.push(typeAlias);
});
return nodes;
}
export function generateOptions(options) {
const nodes = [];
const optionNames = Object.keys(options);
// generate type aliases for each option (all unit values)
optionNames.forEach((name) => {
const option = options[name];
const names = Object.keys(option);
const unit = ts.factory.createTypeAliasDeclaration([
ts.factory.createModifier(ts.SyntaxKind.ExportKeyword),
], capitalize(name), undefined, ts.factory.createUnionTypeNode(names.map(n => ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(unicodeToChar(n))))));
nodes.push(unit);
});
nodes.push(line());
// generate FitOptions interface which contains all options and their respective types (from above)
const fitOptions = ts.factory.createInterfaceDeclaration([
ts.factory.createModifier(ts.SyntaxKind.ExportKeyword),
], 'FitOptions', undefined, undefined, [
...optionNames.map(name => generateProperty(name, ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('Unit'), [ts.factory.createTypeReferenceNode(ts.factory.createIdentifier(capitalize(name)))]), false)),
]);
nodes.push(fitOptions);
return nodes;
}
export function generateUtilities() {
const nodes = [];
const unitType = ts.factory.createTypeAliasDeclaration([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], 'Unit', [ts.factory.createTypeParameterDeclaration(undefined, 'T', ts.factory.createTypeReferenceNode('string'))], ts.factory.createTypeReferenceNode('Record', [
ts.factory.createTypeReferenceNode('T'),
ts.factory.createTypeLiteralNode([
generateProperty('multiplier', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
generateProperty('offset', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
]),
]));
nodes.push(unitType);
const messageIndex = ts.factory.createInterfaceDeclaration([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], 'MessageIndex', undefined, undefined, [
generateProperty('0', ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword), false),
generateProperty('value', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
generateProperty('reserved', ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword), false),
generateProperty('selected', ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword), false),
]);
nodes.push(messageIndex);
return nodes;
}
function generateAdditionalFields(msg) {
if (msg.name === 'lap') {
return [
generateArrayProperty('records', ts.factory.createTypeReferenceNode(snakeToCamel('parsed_record'))),
generateArrayProperty('lengths', ts.factory.createTypeReferenceNode(snakeToCamel('parsed_length'))),
];
}
if (msg.name === 'session') {
return [
generateArrayProperty('laps', ts.factory.createTypeReferenceNode(snakeToCamel('parsed_lap'))),
];
}
if (msg.name === 'activity') {
const props = {
sessions: 'parsed_session',
events: 'parsed_event',
hrv: 'parsed_hrv',
device_infos: 'parsed_device_info',
developer_data_ids: 'parsed_developer_data_id',
field_descriptions: 'parsed_field_description',
sports: 'parsed_sport',
splits: 'parsed_split',
split_summaries: 'parsed_split_summary',
};
return Object.keys(props).map(prop => generateArrayProperty(prop, ts.factory.createTypeReferenceNode(snakeToCamel(props[prop]))));
}
return [];
}
export function generateFitType() {
const collectionProperties = {
laps: 'parsed_lap',
records: 'parsed_record',
sessions: 'parsed_session',
lengths: 'parsed_length',
events: 'parsed_event',
device_infos: 'parsed_device_info',
developer_data_ids: 'parsed_developer_data_id',
field_descriptions: 'parsed_field_description',
hrv: 'parsed_hrv',
hr_zone: 'parsed_hr_zone',
power_zone: 'parsed_power_zone',
dive_gases: 'parsed_dive_gas',
course_points: 'parsed_course_point',
sports: 'parsed_sport',
monitors: 'parsed_monitoring',
stress: 'parsed_stress_level',
file_ids: 'parsed_file_id',
monitor_info: 'parsed_monitoring_info',
definitions: 'unknown',
tank_updates: 'parsed_tank_update',
tank_summaries: 'parsed_tank_summary',
jumps: 'parsed_jump',
splits: 'parsed_split',
split_summaries: 'parsed_split_summary',
time_in_zone: 'parsed_time_in_zone',
activity_metrics: 'parsed_activity_metrics',
user_metrics: 'parsed_user_metrics',
};
const referenceProperties = {
file_creator: 'parsed_file_creator',
device_settings: 'parsed_device_settings',
dive_summary: '?parsed_dive_summary',
dive_settings: '?parsed_dive_settings',
software: 'parsed_software',
user_profile: 'parsed_user_profile',
activity: 'parsed_activity',
zones_target: '?parsed_zones_target',
};
return ts.factory.createInterfaceDeclaration([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], 'ParsedFit', undefined, undefined, [
generateProperty('protocolVersion', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword)),
generateProperty('profileVersion', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword)),
...Object.keys(referenceProperties).map(prop => generateProperty(prop, ts.factory.createTypeReferenceNode(snakeToCamel(referenceProperties[prop].replace('?', ''))), referenceProperties[prop].startsWith('?'))),
...Object.keys(collectionProperties).map(prop => generateArrayProperty(prop, collectionProperties[prop] === 'unknown'
? ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword)
: ts.factory.createTypeReferenceNode(snakeToCamel(collectionProperties[prop])))),
]);
}
export function generateMessages(messages) {
const nodes = [];
Object.keys(messages).forEach((name) => {
const msg = FIT.messages[Number(name)];
const usedFields = new Set();
const messageType = ts.factory.createInterfaceDeclaration([
ts.factory.createModifier(ts.SyntaxKind.ExportKeyword),
], snakeToCamel(`parsed_${msg.name}`), undefined, undefined, [
...Object.keys(msg).filter(n => n !== 'name').reduce((acc, id) => {
const def = msg[Number(id)];
if (!usedFields.has(def.field)) {
usedFields.add(def.field);
acc.push(generateProperty(def.field, generateTypeFromField(def), !['start_time', 'timestamp'].includes(def.field)));
}
return acc;
}, []),
...generateAdditionalFields(msg),
]);
nodes.push(messageType);
});
return nodes;
}
export function main() {
const sourceFile = ts.createSourceFile('', '', ts.ScriptTarget.Latest);
const nodes = [];
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
nodes.push(header());
nodes.push(...comment('utility types used internally'));
nodes.push(...generateUtilities());
nodes.push(...comment('parsed from Fit.types'));
nodes.push(...generateTypes(FIT.types));
nodes.push(...comment('parsed from Fit.options'));
nodes.push(...generateOptions(FIT.options));
nodes.push(...comment('parsed from Fit.messages'));
nodes.push(...generateMessages(FIT.messages));
nodes.push(...comment('the returned type after parsing a .fit file'));
nodes.push(generateFitType());
const nodesArray = ts.factory.createNodeArray(nodes);
return printer.printList(ts.ListFormat.MultiLine, nodesArray, sourceFile);
}