UNPKG

fit-file-parser

Version:

Parse your .FIT files easily, directly from JS (Garmin, Polar, Suunto)

259 lines (258 loc) 11.6 kB
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); }