diff --git a/package.json b/package.json index 894e4ff..082755d 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "main": "distribution/index.js", "scripts": { "build": "tsc", - "start": "node distribution/index.js", + "start": "node distribution/src/index.js", "test": "vitest" }, "dependencies": { diff --git a/src/sequencer.ts b/src/sequencer.ts index d243a18..d9d5482 100644 --- a/src/sequencer.ts +++ b/src/sequencer.ts @@ -1,4 +1,4 @@ -import { OpenWeatherAPI, type CurrentWeather } from 'openweather-api-node'; +import { OpenWeatherAPI, type DailyWeather } from 'openweather-api-node'; import { voiceLines } from './voice.js'; import type { Config } from './index.js'; import type { Voice } from './voice.js'; @@ -19,7 +19,7 @@ function selectOne(arr: T[]): T { return arr[Math.floor(Math.random() * arr.length)]; } -function resolveSide(side: string, currentWeather: CurrentWeather) { +function resolveSide(side: string, currentWeather: DailyWeather) { if (!side.startsWith('weather')) { return side.includes('.') ? parseFloat(side) : parseInt(side); } @@ -30,13 +30,23 @@ function resolveSide(side: string, currentWeather: CurrentWeather) { return typeof w === 'object' ? JSON.stringify(w) : w as (string | number); } -function conditionIsMet(condition: string | undefined, currentWeather: CurrentWeather): boolean { +function notNotANumber(something: number | string, defaultVal: string) { + if (typeof something === 'string' || !isNaN(something)) { + return something; + } + return defaultVal; +} + +function conditionIsMet(condition: string | undefined, currentWeather: DailyWeather): boolean { if (typeof condition !== 'string') { return true; } const [lhs, relational, rhs] = condition.split(' '); - const lhsResolved = resolveSide(lhs, currentWeather); - const rhsResolved = resolveSide(rhs, currentWeather); + if (lhs === undefined || relational === undefined || rhs === undefined) { + throw new Error(`Condition "${condition}" is not in the correct format`); + } + const lhsResolved = notNotANumber(resolveSide(lhs, currentWeather), lhs); + const rhsResolved = notNotANumber(resolveSide(rhs, currentWeather), rhs); switch (relational) { case '=': case '==': @@ -56,7 +66,7 @@ function conditionIsMet(condition: string | undefined, currentWeather: CurrentWe } } -function resolveMacro(str: string, currentWeather: CurrentWeather): string[] { +function resolveMacro(str: string, currentWeather: DailyWeather): string[] { if (str.startsWith('%')) { const [profile, subject] = str.substring(1).split(' ', 2); const voiceProfile: Voice = config.voices[profile]; @@ -64,18 +74,15 @@ function resolveMacro(str: string, currentWeather: CurrentWeather): string[] { subject.split('.').forEach(t => resolvedSubject = resolvedSubject[t]); return voiceLines(voiceProfile, resolvedSubject); } - else if (str.startsWith('$')) { - return null; - } return [str]; } -function processSequence(sequence: Sequence, currentWeather: CurrentWeather): string[] { +function processSequence(sequence: Sequence, currentWeather: DailyWeather): string[] { const tracks = sequence.tracks; return tracks.map(t => resolveMacro(t, currentWeather)).flat().filter(t => t !== null); } -function processSegment(segment: SegmentName, currentWeather: CurrentWeather): string[] { +function processSegment(segment: SegmentName, currentWeather: DailyWeather): string[] { if (!(segment in config.segments)) { return (config.sequences[segment].conditions || []).every(c => conditionIsMet(c, currentWeather)) ? processSequence(config.sequences[segment], currentWeather) : []; } @@ -89,8 +96,7 @@ function processSegment(segment: SegmentName, currentWeather: CurrentWeather): s async function Sequencer(conf: Config): Promise { config = conf; const weather = new OpenWeatherAPI(conf.weather); - const currentWeather = await weather.getCurrent(); - console.log(JSON.stringify(currentWeather)); + const currentWeather = await weather.getToday(); const sequence: string[] = []; const program: SegmentName[] = selectOne(conf.programs); for (let i = 0; i < program.length; i++) { diff --git a/test/sequencer.test.ts b/test/sequencer.test.ts index 99b1f09..1b5cf6c 100644 --- a/test/sequencer.test.ts +++ b/test/sequencer.test.ts @@ -9,7 +9,7 @@ vi.mock('openweather-api-node', () => { return { OpenWeatherAPI: vi.fn().mockImplementation((_) => { return { - getCurrent: vi.fn(() => { + getToday: vi.fn(() => { return { "lat": 43.0748, "lon": -89.3838, @@ -106,5 +106,85 @@ describe('sequencer', () => { weather: dummyWeather })).to.be.ordered.members(['seq1.flac', 'seq1.flac']); }); + + it('throws an error on invalid conditions', async () => { + await expect(Sequencer({ + programs: [['sequence 1']], + segments: { + }, + sequences: { + 'sequence 1': { + 'conditions': ['100'], + 'tracks': [ + 'seq1.flac' + ] + } + }, + voices: {}, + weather: dummyWeather + })).rejects.toThrow(/not in the correct format/); + + await expect(Sequencer({ + programs: [['sequence 1']], + segments: { + }, + sequences: { + 'sequence 1': { + 'conditions': ['1 ~ 2'], + 'tracks': [ + 'seq1.flac' + ] + } + }, + voices: {}, + weather: dummyWeather + })).rejects.toThrow(/Unsupported relational operator/); + }); + + it('can stringify conditions', async () => { + expect(await Sequencer({ + programs: [['sequence 1']], + segments: { + }, + sequences: { + 'sequence 1': { + 'conditions': ['weather.feelsLike = {"cur":55.31}'], + 'tracks': [ + 'seq1.flac' + ] + } + }, + voices: {}, + weather: dummyWeather + })).to.be.ordered.members(['seq1.flac']); + }); + + it('can parse voice macros', async () => { + expect(await Sequencer({ + programs: [['sequence 1']], + segments: { + }, + sequences: { + 'sequence 1': { + 'tracks': [ + '%alice weather.temp.max' + ] + } + }, + voices: { + "alice": { + "directory": "alice/", + "extension": "flac" + } + }, + weather: dummyWeather + })).to.be.ordered.members([ + 'alice/fifty.flac', + 'alice/eight.flac', + 'alice/point.flac', + 'alice/zero.flac', + 'alice/one.flac' + ]); + }); });