Skip to content

Commit

Permalink
sdk: Add strict types to sdk (#1308)
Browse files Browse the repository at this point in the history
* Enable strict mode

* Add @types/node

Remove @types/rimraf

* Fix `include` path to be actual `src`

* Add strict to `sdk`

* Assert `getItem`

* Fix types in SDK

* Refactor SDK function to be type safe

* parseValue handle value null or undefined

* Fix types tsconfig

* Make getDeviceConsole required

* Add build-sdk workflow

* Set working directory

* Assert not undefined

* Remove optionals

* Undo addScryptedInterfaceProperties, revert to self executing function

* Use different type

* Make _deviceState private and add ts-ignore

* Remove unused function

* Remove non-null asserts

* Add tsconfig for sdk/types/src

* Get property isOptional from schema

Use typedoc types

* Type fixes

* Fix type

* Fix type

* Revert change
  • Loading branch information
longzheng committed Feb 15, 2024
1 parent 426454f commit dd7d920
Show file tree
Hide file tree
Showing 10 changed files with 98 additions and 94 deletions.
25 changes: 25 additions & 0 deletions .github/workflows/build-sdk.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Build SDK

on:
push:
branches: ["main"]
paths: ["sdk/**"]
pull_request:
paths: ["sdk/**"]
workflow_dispatch:

jobs:
build:
name: Build
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./sdk

steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
node-version: 18
- run: npm ci
- run: npm run build
47 changes: 24 additions & 23 deletions sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import { DeviceBase, ScryptedInterfaceDescriptors, ScryptedInterfaceProperty, TY
* @category Core Reference
*/
export class ScryptedDeviceBase extends DeviceBase {
private _storage: Storage;
private _log: Logger;
private _console: Console;
private _deviceState: DeviceState;
private _storage: Storage | undefined;
private _log: Logger | undefined;
private _console: Console | undefined;
private _deviceState: DeviceState | undefined;

constructor(public readonly nativeId?: string) {
super();
Expand Down Expand Up @@ -43,7 +43,7 @@ export class ScryptedDeviceBase extends DeviceBase {
});
}

getMediaObjectConsole(mediaObject: MediaObject): Console {
getMediaObjectConsole(mediaObject: MediaObject): Console | undefined {
if (typeof mediaObject.sourceId !== 'string')
return this.console;
return deviceManager.getMixinConsole(mediaObject.sourceId, this.nativeId);
Expand Down Expand Up @@ -86,17 +86,17 @@ export interface MixinDeviceOptions<T> {
mixinProviderNativeId: ScryptedNativeId;
mixinDevice: T;
mixinDeviceInterfaces: ScryptedInterface[];
private _storage: Storage;
private mixinStorageSuffix: string;
private _log: Logger;
private _console: Console;
private _storage: Storage | undefined;
private mixinStorageSuffix: string | undefined;
private _log: Logger | undefined;
private _console: Console | undefined;
private _deviceState: WritableDeviceState;
private _listeners = new Set<EventListenerRegister>();

constructor(options: MixinDeviceOptions<T>) {
super();

this.nativeId = systemManager.getDeviceById(this.id)?.nativeId;
this.nativeId = systemManager.getDeviceById(this.id).nativeId;
this.mixinDevice = options.mixinDevice;
this.mixinDeviceInterfaces = options.mixinDeviceInterfaces;
this.mixinStorageSuffix = options.mixinStorageSuffix;
Expand Down Expand Up @@ -164,40 +164,41 @@ export interface MixinDeviceOptions<T> {
}
}


(function () {
function _createGetState(state: any) {
function _createGetState<T>(deviceBase: ScryptedDeviceBase | MixinDeviceBase<T>, state: ScryptedInterfaceProperty) {
return function () {
this._lazyLoadDeviceState();
return this._deviceState?.[state];
deviceBase._lazyLoadDeviceState();
// @ts-ignore: accessing private property
return deviceBase._deviceState?.[state];
};
}

function _createSetState(state: any) {
function _createSetState<T>(deviceBase: ScryptedDeviceBase | MixinDeviceBase<T>, state: ScryptedInterfaceProperty) {
return function (value: any) {
this._lazyLoadDeviceState();
if (!this._deviceState)
deviceBase._lazyLoadDeviceState();
// @ts-ignore: accessing private property
if (!deviceBase._deviceState)
console.warn('device state is unavailable. the device must be discovered with deviceManager.onDeviceDiscovered or deviceManager.onDevicesChanged before the state can be set.');
else
this._deviceState[state] = value;
// @ts-ignore: accessing private property
deviceBase._deviceState[state] = value;
};
}

for (const field of Object.values(ScryptedInterfaceProperty)) {
if (field === ScryptedInterfaceProperty.nativeId)
continue;
Object.defineProperty(ScryptedDeviceBase.prototype, field, {
set: _createSetState(field),
get: _createGetState(field),
set: _createSetState(ScryptedDeviceBase.prototype, field),
get: _createGetState(ScryptedDeviceBase.prototype, field),
});
Object.defineProperty(MixinDeviceBase.prototype, field, {
set: _createSetState(field),
get: _createGetState(field),
set: _createSetState(MixinDeviceBase.prototype, field),
get: _createGetState(MixinDeviceBase.prototype, field),
});
}
})();


export const sdk: ScryptedStatic = {} as any;
declare const deviceManager: DeviceManager;
declare const endpointManager: EndpointManager;
Expand Down
6 changes: 5 additions & 1 deletion sdk/src/storage-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import sdk, { ScryptedInterface, Setting, Settings, SettingValue } from ".";

const { systemManager } = sdk;

function parseValue(value: string, setting: StorageSetting, readDefaultValue: () => any, rawDevice?: boolean) {
function parseValue(value: string | null | undefined, setting: StorageSetting, readDefaultValue: () => any, rawDevice?: boolean) {
if (value === null || value === undefined) {
return readDefaultValue();
}

const type = setting.multiple ? 'array' : setting.type;

if (type === 'boolean') {
Expand Down
3 changes: 2 additions & 1 deletion sdk/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"esModuleInterop": true,
"sourceMap": true,
"declaration": true,
"outDir": "dist"
"outDir": "dist",
"strict": true
},
"include": [
"src/**/*",
Expand Down
76 changes: 21 additions & 55 deletions sdk/types/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions sdk/types/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
"license": "ISC",
"scripts": {
"prepublishOnly": "npm run build",
"build": "rimraf dist gen && typedoc && ts-node ./src/build.ts && tsc"
"build": "tsc --project src && rimraf dist gen && typedoc && ts-node ./src/build.ts && tsc"
},
"devDependencies": {
"@types/rimraf": "^3.0.2",
"@types/node": "^18.19.15",
"rimraf": "^3.0.2",
"ts-node": "^10.9.1"
},
Expand Down
23 changes: 14 additions & 9 deletions sdk/types/src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ function toTypescriptType(type: any): string {
}

for (const name of Object.values(ScryptedInterface)) {
const td = schema.children.find((child) => child.name === name);
const children = td.children || [];
const td = schema.children?.find((child) => child.name === name);
const children = td?.children || [];
const properties = children.filter((child) => child.kindString === 'Property').map((child) => child.name);
const methods = children.filter((child) => child.kindString === 'Method').map((child) => child.name);
ScryptedInterfaceDescriptors[name] = {
Expand All @@ -46,7 +46,7 @@ ${Object.entries(allProperties).map(([property, {type, flags}]) => ` ${property
}
export class DeviceBase implements DeviceState {
${Object.entries(allProperties).map(([property, {type, flags}]) => ` ${property}${flags.isOptional ? '?' : ''}: ${toTypescriptType(type)}`).join('\n')};
${Object.entries(allProperties).map(([property, {type, flags}]) => ` ${property}${flags.isOptional ? '?' : '!'}: ${toTypescriptType(type)}`).join('\n')};
}
`;

Expand Down Expand Up @@ -138,12 +138,15 @@ function selfSignature(method: any) {
return params.join(', ');
}

const enums = schema.children.filter((child: any) => child.kindString === 'Enumeration');
const interfaces = schema.children.filter((child: any) => Object.values(ScryptedInterface).includes(child.name));
const enums = schema.children?.filter((child: any) => child.kindString === 'Enumeration') ?? [];
const interfaces = schema.children?.filter((child: any) => Object.values(ScryptedInterface).includes(child.name)) ?? [];
let python = '';

for (const iface of ['Logger', 'DeviceManager', 'SystemManager', 'MediaManager', 'EndpointManager']) {
interfaces.push(schema.children.find((child: any) => child.name === iface));
const child = schema.children?.find((child: any) => child.name === iface);

if (child)
interfaces.push(child);
}

let seen = new Set<string>();
Expand Down Expand Up @@ -226,14 +229,16 @@ for (const td of interfaces) {

let pythonEnums = ''
for (const e of enums) {
pythonEnums += `
if (e.children) {
pythonEnums += `
class ${e.name}(str, Enum):
${toDocstring(e)}
`
for (const val of e.children) {
if ('type' in val && 'value' in val.type)
if (val.type && 'value' in val.type)
pythonEnums += ` ${val.name} = "${val.type.value}"
`;
}
}
}

Expand Down Expand Up @@ -282,7 +287,7 @@ ScryptedInterfaceDescriptors = ${JSON.stringify(ScryptedInterfaceDescriptors, nu
`

while (discoveredTypes.size) {
const unknowns = schema.children.filter((child: any) => discoveredTypes.has(child.name) && !enums.find((e: any) => e.name === child.name));
const unknowns = schema.children?.filter((child: any) => discoveredTypes.has(child.name) && !enums.find((e: any) => e.name === child.name)) ?? [];

const newSeen = new Set([...seen, ...discoveredTypes]);
discoveredTypes.clear();
Expand Down
3 changes: 2 additions & 1 deletion sdk/types/src/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"noImplicitAny": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"noEmit": true
"noEmit": true,
"strict": true
}
}
2 changes: 1 addition & 1 deletion sdk/types/src/types.input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1201,7 +1201,7 @@ export interface AmbientLightSensor {
/**
* The ambient light in lux.
*/
ambientLight: number;
ambientLight?: number;
}
export interface OccupancySensor {
occupied?: boolean;
Expand Down
3 changes: 2 additions & 1 deletion sdk/types/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"esModuleInterop": true,
"sourceMap": true,
"declaration": true,
"outDir": "dist"
"outDir": "dist",
"strict": true
},
"include": [
"gen/**/*"
Expand Down

0 comments on commit dd7d920

Please sign in to comment.