Much more strictly define types and use type guards

This commit is contained in:
Adam Goldsmith 2019-12-26 14:32:44 -05:00
parent e684dfcd04
commit ae62484d30
2 changed files with 156 additions and 30 deletions

View File

@ -15,7 +15,7 @@
v-for="(event, index) in minion_events" v-for="(event, index) in minion_events"
:key="index" :key="index"
> >
<summary>{{ event.tag }}</summary> <summary>{{ event_name(event) }}</summary>
<pre>{{ JSON.stringify(event, null, 2) }}</pre> <pre>{{ JSON.stringify(event, null, 2) }}</pre>
</details> </details>
</details> </details>
@ -29,14 +29,33 @@ import { Vue, Component, Prop, Ref } from 'vue-property-decorator';
import credentials from './credentials.json'; import credentials from './credentials.json';
import * as salt from './salt';
function isEventType<T extends salt.SaltEvent['splitTag'][1]>(
event: salt.SaltEvent,
type: T
): event is Extract<salt.SaltEvent, { splitTag: ['salt', T, ...any[]] }> {
return event.splitTag[1] === type;
}
function isJobEventType<T extends salt.SaltEvent['splitTag'][3]>(
event: salt.SaltEvent,
type: T
): event is Extract<
salt.SaltEvent,
{ splitTag: ['salt', 'job', any, T, ...any[]] }
> {
return event.splitTag[3] === type;
}
const BASE_URL = 'https://salt.sawtooth.claremontmakerspace.org:8000/'; const BASE_URL = 'https://salt.sawtooth.claremontmakerspace.org:8000/';
@Component @Component
export default class App extends Vue { export default class App extends Vue {
evtSource: EventSource | null = null; evtSource: EventSource | null = null;
events: any[] = []; events: salt.SaltEvent[] = [];
mounted() { mounted(): void {
fetch(BASE_URL + 'login', { fetch(BASE_URL + 'login', {
method: 'POST', method: 'POST',
mode: 'cors', mode: 'cors',
@ -44,50 +63,58 @@ export default class App extends Vue {
}) })
.then(r => r.json()) .then(r => r.json())
.then(r => { .then(r => {
const token = r.return[0].token; const token: string = r.return[0].token;
this.evtSource = new EventSource(BASE_URL + 'events?token=' + token); this.evtSource = new EventSource(BASE_URL + 'events?token=' + token);
this.evtSource.onmessage = e => { this.evtSource.onmessage = e => {
this.events.push(JSON.parse(e.data)); const evt = JSON.parse(e.data);
evt.splitTag = evt.tag.split('/');
this.events.push(evt);
}; };
}); });
} }
parse_tag(tag) { event_name(event: salt.JobEvent) {
// todo: would probably be easier as string splitting at this point if (isJobEventType(event, 'prog')) {
const match = tag.match(/salt\/job\/([^\/]*)\/(prog|ret)\/([^\/]*)\/?(.*)/); return `${this.event_symbol(event)} Progress: ${
if (!match) return null; event.data.data.ret.name
return { }|${event.data.data.ret.duration} `;
jid: match[1], } else if (isJobEventType(event, 'ret')) {
type: match[2], return `${this.event_symbol(event)} Return: ${event.data.fun} `;
minion: match[3], }
prog_id: match.length >= 5 ? match[4] : null,
};
} }
event_symbol(event) { event_symbol(event: salt.JobEvent) {
const tag = this.parse_tag(event.tag); if (isJobEventType(event, 'prog')) {
if (tag.type === 'prog') {
const ret = event.data.data.ret; const ret = event.data.data.ret;
if (!ret.result) return '✗'; if (!ret.result) return '✗';
else if ('ret' in ret.changes) return 'Δ'; else if (Object.keys(ret.changes).length !== 0) return 'Δ';
else return '✓'; else return '✓';
} else if (tag.type === 'ret') { } else if (isJobEventType(event, 'ret')) {
return event.data.success ? '✓' : '✗'; return event.data.success ? '✓' : '✗';
} }
return '?'; return '?';
} }
get jobs() { // {jid: {mid: event[]}}
return this.events.reduce((acc, e) => { get jobs(): { [key: string]: { [key: string]: salt.JobEvent[] } } {
const tag = this.parse_tag(e.tag); return this.events.reduce(
if (tag) { (
if (!(tag.jid in acc)) acc[tag.jid] = {}; acc: { [key: string]: { [key: string]: salt.JobEvent[] } },
if (!(tag.minion in acc[tag.jid])) acc[tag.jid][tag.minion] = []; e: salt.SaltEvent
acc[tag.jid][tag.minion].push(e); ) => {
} if (
return acc; isEventType(e, 'job') &&
}, {}); (isJobEventType(e, 'prog') || isJobEventType(e, 'ret'))
) {
if (!(e.data.jid in acc)) acc[e.data.jid] = {};
if (!(e.data.id in acc[e.data.jid])) acc[e.data.jid][e.data.id] = [];
acc[e.data.jid][e.data.id].push(e);
}
return acc;
},
{}
);
} }
} }
</script> </script>
@ -96,4 +123,8 @@ export default class App extends Vue {
.minion .event { .minion .event {
margin-left: 2ex; margin-left: 2ex;
} }
pre {
white-space: pre-wrap;
}
</style> </style>

95
src/salt.d.ts vendored Normal file
View File

@ -0,0 +1,95 @@
export type MinionID = string;
export type JobID = string;
export type SaltEvent = AuthenticationEvent | StartEvent | KeyEvent | JobEvent;
// | RunnerEvent
// | PresenceEvent
// | CloudEvent;
export interface AuthenticationEvent {
tag: 'salt/auth';
splitTag: ['salt', 'auth'];
data: {
id: MinionID;
act: 'accept' | 'pend' | 'reject';
pub: string;
};
}
export interface StartEvent {
tag: string; // salt/minion/<MID>/start
splitTag: ['salt', 'minion', string, 'start'];
data: {
id: MinionID;
};
}
export interface KeyEvent {
tag: 'salt/key';
splitTag: ['salt', 'key'];
data: {
id: MinionID;
act: 'accept' | 'delete';
};
}
export type JobEvent = JobNewEvent | JobRetEvent | JobProgEvent;
export interface JobNewEvent {
tag: string; // salt/job/<JID>/new
splitTag: ['salt', 'job', string, 'new'];
data: {
jid: JobID;
tgt: string;
tgt_type: 'glob' | 'grain' | 'compound' | string;
fun: string;
arg: [string];
minions: [MinionID];
user: string;
};
}
export interface JobRetEvent {
tag: string; // salt/job/<JID>/ret/<MID>
splitTag: ['salt', 'job', string, 'ret', string];
data: {
id: MinionID;
jid: JobID;
retcode: number;
fun: string;
fun_args: [string];
success: boolean;
return: any;
};
}
export interface JobProgEvent {
tag: string; // salt/job/<JID>/prog/<MID>/<RUN NUM>
splitTag: ['salt', 'job', string, 'prog', string, string];
data: {
data: any;
id: MinionID;
jid: JobID;
};
}
// TODO
export interface RunnerEvent {
tag: string;
splitTag: [];
data: {};
}
// TODO
export interface PresenceEvent {
tag: string;
splitTag: [];
data: {};
}
// TODO
export interface CloudEvent {
tag: string;
splitTag: [];
data: {};
}