Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stronger typings and simpler variable parsing #76

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
"nanoid": "^3.3.4",
"p-queue": "^6.6.2",
"p-timeout": "^4.1.0",
"tslib": "^2.6.2"
"tslib": "^2.6.2",
"type-fest": "^4.15.0"
},
"devDependencies": {
"@companion-module/tools": "^1.5.0",
Expand Down
72 changes: 72 additions & 0 deletions src/internal/strict-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type { ConditionalKeys } from 'type-fest'
import type { CompanionOptionValues, CompanionActionContext } from '../module-api/index.js'
import type { CompanionCommonCallbackContext, StrictOptions, StrictOptionsObject } from '../module-api/common.js'

export class StrictOptionsImpl<TOptions> implements StrictOptions<TOptions> {
readonly #options: any
readonly #context: CompanionCommonCallbackContext
readonly #fields: StrictOptionsObject<TOptions, any>

constructor(
options: CompanionOptionValues,
context: CompanionActionContext,
fields: StrictOptionsObject<TOptions, any>
) {
this.#options = options
this.#context = context
this.#fields = fields
}

getRawJson(): any {
return { ...this.#options }
}
getRaw<Key extends keyof TOptions>(fieldName: Key): any {
// TODO - should this populate defaults?
return this.#options[fieldName]
}

getPlainString<Key extends ConditionalKeys<TOptions, string>>(fieldName: Key): TOptions[Key] {
const fieldSpec = this.#fields[fieldName]
const defaultValue = fieldSpec && 'default' in fieldSpec ? fieldSpec.default : undefined

const rawValue = this.#options[fieldName]
if (defaultValue !== undefined && rawValue === undefined) return String(defaultValue) as any

return String(rawValue) as any
}

getPlainNumber<Key extends ConditionalKeys<TOptions, number>>(fieldName: Key): TOptions[Key] {
const fieldSpec = this.#fields[fieldName]
const defaultValue = fieldSpec && 'default' in fieldSpec ? fieldSpec.default : undefined

const rawValue = this.#options[fieldName]
if (defaultValue !== undefined && rawValue === undefined) return Number(defaultValue) as any

const value = Number(rawValue)
if (isNaN(value)) {
throw new Error(`Invalid option '${String(fieldName)}'`)
}
return value as any
}

getPlainBoolean<Key extends ConditionalKeys<TOptions, boolean>>(fieldName: Key): boolean {
const fieldSpec = this.#fields[fieldName]
const defaultValue = fieldSpec && 'default' in fieldSpec ? fieldSpec.default : undefined

const rawValue = this.#options[fieldName]
if (defaultValue !== undefined && rawValue === undefined) return Boolean(defaultValue)

return Boolean(rawValue)
}

async getParsedString<Key extends ConditionalKeys<TOptions, string | undefined>>(fieldName: Key): Promise<string> {
const rawValue = this.#options[fieldName]

return this.#context.parseVariablesInString(rawValue)
}
async getParsedNumber<Key extends ConditionalKeys<TOptions, string | undefined>>(fieldName: Key): Promise<number> {
const str = await this.getParsedString(fieldName)

return Number(str)
}
}
60 changes: 59 additions & 1 deletion src/module-api/action.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { CompanionCommonCallbackContext } from './common.js'
import type { CompanionCommonCallbackContext, StrictOptions, StrictOptionsObject } from './common.js'
import type {
CompanionOptionValues,
CompanionInputFieldCheckbox,
Expand Down Expand Up @@ -30,6 +30,8 @@ export type CompanionActionContext = CompanionCommonCallbackContext
* The definition of an action
*/
export interface CompanionActionDefinition {
type?: 'loose'

/** Name to show in the actions list */
name: string
/** Additional description of the action */
Expand Down Expand Up @@ -93,3 +95,59 @@ export interface CompanionActionEvent extends CompanionActionInfo {
/** Identifier of the surface which triggered this action */
readonly surfaceId: string | undefined
}

/**
* Basic information about an instance of an action
*/
export interface StrictActionInfo<TOptions> {
/** The unique id for this action */
readonly id: string
/** The unique id for the location of this action */
readonly controlId: string
/** The id of the action definition */
readonly actionId: string
/** The user selected options for the action */
readonly options: StrictOptions<TOptions>
}
/**
* Extended information for execution of an action
*/
export interface StrictActionEvent<TOptions> extends StrictActionInfo<TOptions> {
/** Identifier of the surface which triggered this action */
readonly surfaceId: string | undefined
}

export interface StrictActionDefinition<TOptions> {
type: 'strict'

/** Name to show in the actions list */
name: string
/** Additional description of the action */
description?: string
/** The input fields for the action */
options: StrictOptionsObject<TOptions, SomeCompanionActionInputField>

/** Called to execute the action */
callback: (action: StrictActionEvent<TOptions>, context: CompanionActionContext) => Promise<void> | void
/**
* Called to report the existence of an action
* Useful to ensure necessary data is loaded
*/
subscribe?: (action: StrictActionInfo<TOptions>, context: CompanionActionContext) => Promise<void> | void
/**
* Called to report an action has been edited/removed
* Useful to cleanup subscriptions setup in subscribe
*/
unsubscribe?: (action: StrictActionInfo<TOptions>, context: CompanionActionContext) => Promise<void> | void
/**
* The user requested to 'learn' the values for this action.
*/
learn?: (
action: StrictActionEvent<TOptions>,
context: CompanionActionContext
) => TOptions | undefined | Promise<TOptions | undefined>
}

export type StrictActionDefinitions<TTypes> = {
[Key in keyof TTypes]: StrictActionDefinition<TTypes[Key]> | undefined
}
21 changes: 21 additions & 0 deletions src/module-api/common.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import type { ConditionalKeys } from 'type-fest'
import type { CompanionInputFieldBase } from './input.js'

/**
* Utility functions available in the context of an action/feedback
*/
Expand All @@ -10,3 +13,21 @@ export interface CompanionCommonCallbackContext {
*/
parseVariablesInString(text: string): Promise<string>
}

export type StrictOptionsObject<TOptions, TFields extends CompanionInputFieldBase> = {
[K in keyof TOptions]: undefined extends TOptions[K] ? TFields | undefined : TFields
}

/**
*
*/
export interface StrictOptions<TOptions> {
getRawJson(): any
getRaw<Key extends keyof TOptions>(fieldName: Key): TOptions[Key] | undefined
getPlainString<Key extends ConditionalKeys<TOptions, string>>(fieldName: Key): TOptions[Key]
getPlainNumber<Key extends ConditionalKeys<TOptions, number>>(fieldName: Key): TOptions[Key]
getPlainBoolean<Key extends ConditionalKeys<TOptions, boolean>>(fieldName: Key): boolean

getParsedString<Key extends ConditionalKeys<TOptions, string | undefined>>(fieldName: Key): Promise<string>
getParsedNumber<Key extends ConditionalKeys<TOptions, string | undefined>>(fieldName: Key): Promise<number>
}
96 changes: 95 additions & 1 deletion src/module-api/feedback.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { CompanionCommonCallbackContext } from './common.js'
import type { CompanionCommonCallbackContext, StrictOptions, StrictOptionsObject } from './common.js'
import type {
CompanionOptionValues,
CompanionInputFieldStaticText,
Expand Down Expand Up @@ -161,3 +161,97 @@ export type CompanionFeedbackDefinition = CompanionBooleanFeedbackDefinition | C
export interface CompanionFeedbackDefinitions {
[id: string]: CompanionFeedbackDefinition | undefined
}

/**
* Basic information about an instance of an Feedback
*/
export interface StrictFeedbackInfo<TOptions> {
/** The type of the feedback */
readonly type: 'boolean-strict' | 'advanced-strict'
/** The unique id for this feedback */
readonly id: string
/** The unique id for the location of this feedback */
readonly controlId: string
/** The id of the feedback definition */
readonly feedbackId: string
/** The user selected options for the feedback */
readonly options: StrictOptions<TOptions>
}
/**
* Extended information for execution of an Feedback
*/
export type StrictBooleanFeedbackEvent<TOptions> = StrictFeedbackInfo<TOptions>

/**
* Extended information for execution of an advanced feedback
*/
export interface StrictAdvancedFeedbackEvent<TOptions> extends StrictFeedbackInfo<TOptions> {
/** If control supports an imageBuffer, the dimensions the buffer should be */
readonly image?: {
readonly width: number
readonly height: number
}
}

export interface StrictFeedbackDefinitionBase<TOptions> {
/** Name to show in the Feedbacks list */
name: string
/** Additional description of the Feedback */
description?: string
/** The input fields for the Feedback */
options: StrictOptionsObject<TOptions, SomeCompanionFeedbackInputField>

/**
* Called to report the existence of an Feedback
* Useful to ensure necessary data is loaded
*/
subscribe?: (Feedback: StrictFeedbackInfo<TOptions>, context: CompanionFeedbackContext) => Promise<void> | void
/**
* Called to report an Feedback has been edited/removed
* Useful to cleanup subscriptions setup in subscribe
*/
unsubscribe?: (Feedback: StrictFeedbackInfo<TOptions>, context: CompanionFeedbackContext) => Promise<void> | void
/**
* The user requested to 'learn' the values for this Feedback.
*/
learn?: (
Feedback: StrictFeedbackInfo<TOptions>,
context: CompanionFeedbackContext
) => TOptions | undefined | Promise<TOptions | undefined>
}

export interface StrictBooleanFeedbackDefinition<TOptions> extends StrictFeedbackDefinitionBase<TOptions> {
type: 'boolean-strict'

/** The default style properties for this feedback */
defaultStyle: Partial<CompanionFeedbackButtonStyleResult>

/**
* If `undefined` or true, Companion will add an 'Inverted' checkbox for your feedback, and handle the logic for you.
* By setting this to false, you can disable this for your feedback. You should do this if it does not make sense for your feedback.
*/
showInvert?: boolean

/** Called to execute the Feedback */
callback: (
Feedback: StrictBooleanFeedbackEvent<TOptions>,
context: CompanionFeedbackContext
) => Promise<boolean> | boolean
}
export interface StrictAdvancedFeedbackDefinition<TOptions> extends StrictFeedbackDefinitionBase<TOptions> {
type: 'advanced-strict'

/** Called to execute the Feedback */
callback: (
Feedback: StrictAdvancedFeedbackEvent<TOptions>,
context: CompanionFeedbackContext
) => Promise<CompanionAdvancedFeedbackResult> | CompanionAdvancedFeedbackResult
}

export declare type StrictFeedbackDefinition<TOption> =
| StrictBooleanFeedbackDefinition<TOption>
| StrictAdvancedFeedbackDefinition<TOption>

export type StrictFeedbackDefinitions<TTypes> = {
[Key in keyof TTypes]: StrictFeedbackDefinition<TTypes[Key]> | undefined
}
71 changes: 71 additions & 0 deletions src/module-api/preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,74 @@ export interface CompanionButtonStepActions {
export interface CompanionPresetDefinitions {
[id: string]: CompanionButtonPresetDefinition | undefined
}

export type StrictPresetDefinitions<TActions, TFeedbacks> = StrictPresetDefinitionCategory<TActions, TFeedbacks>[]
export type StrictPresetDefinitionCategory<TActions, TFeedbacks> = {
name: string
presets: Record<string, StrictButtonPresetDefinition<TActions, TFeedbacks>>
}

export interface StrictButtonPresetDefinition<TActions, TFeedbacks> {
/** The type of this preset */
type: 'button-strict'
/** The category of this preset, for grouping */
// category: string
/** The name of this preset */
name: string
/** The base style of this preset, this will be copied to the button */
style: CompanionButtonStyleProps
/** Preview style for preset, will be used in GUI for preview */
previewStyle?: CompanionButtonStyleProps
/** Options for this preset */
options?: CompanionButtonPresetOptions
/** The feedbacks on the button */
feedbacks: StrictPresetFeedback<TFeedbacks>[]
steps: StrictButtonStepActions<TActions>[]
}

type StrictPresetFeedbackInner<TTypes, Id extends keyof TTypes> = Id extends any
? {
/** The id of the feedback definition */
feedbackId: Id
/** The option values for the feedback */
options: TTypes[Id]
/**
* If a boolean feedback, the style effect of the feedback
*/
style?: CompanionFeedbackButtonStyleResult
/**
* If a boolean feedback, invert the value of the feedback
*/
isInverted?: boolean
}
: never

export type StrictPresetFeedback<TFeedbacks> = StrictPresetFeedbackInner<TFeedbacks, keyof TFeedbacks>

type StrictPresetActionInner<TTypes, Id extends keyof TTypes> = Id extends any
? {
/** The id of the action definition */
actionId: Id
/** The option values for the action */
options: TTypes[Id]
/** The execution delay of the action */
delay?: number
}
: never

export type StrictPresetAction<TActions> = StrictPresetActionInner<TActions, keyof TActions>

export interface StrictButtonStepActions<TActions> {
/** The button down actions */
down: StrictPresetAction<TActions>[]
/** The button up actions */
up: StrictPresetAction<TActions>[]
rotateLeft?: StrictPresetAction<TActions>[]
rotateRight?: StrictPresetAction<TActions>[]
[delay: number]: StrictPresetActionsWithOptions<TActions> | StrictPresetAction<TActions>[]
}

export interface StrictPresetActionsWithOptions<TActions> {
options?: CompanionActionSetOptions
actions: StrictPresetAction<TActions>[]
}
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3932,6 +3932,11 @@ type-fest@^0.21.3:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==

type-fest@^4.15.0:
version "4.15.0"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.15.0.tgz#21da206b89c15774cc718c4f2d693e13a1a14a43"
integrity sha512-tB9lu0pQpX5KJq54g+oHOLumOx+pMep4RaM6liXh2PKmVRFF+/vAtUP0ZaJ0kOySfVNjF6doBWPHhBhISKdlIA==

type@^2.7.2:
version "2.7.2"
resolved "https://registry.yarnpkg.com/type/-/type-2.7.2.tgz#2376a15a3a28b1efa0f5350dcf72d24df6ef98d0"
Expand Down