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

feature: Add Feature discovery API endpoint and plugin interface #1630

Merged
merged 27 commits into from
Apr 22, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a892979
Features API
panaaj Sep 20, 2023
efa4d23
Create interface for plugins.
panaaj Sep 22, 2023
aa679e1
chore: format
panaaj Sep 22, 2023
9135032
Add getFeatures and registerFeature definitions
panaaj Sep 23, 2023
be80e2a
Only add api feature to apiList once
panaaj Sep 26, 2023
6f93c91
Merge branch 'master' into features_api
panaaj Sep 27, 2023
072fbd2
API feature registration moved to API constructor.
panaaj Mar 22, 2024
713f306
Add option to show only enabled plugins.
panaaj Mar 29, 2024
42de518
Remove registration method.
panaaj Mar 30, 2024
0a813eb
chore: formatting
panaaj Mar 30, 2024
82fbe6e
typings and API name list
panaaj Mar 31, 2024
e222331
align with discovery OpenApi
panaaj Apr 2, 2024
5bb8a50
addressed review comments
panaaj Apr 9, 2024
c39e4cc
SignalKApiId is type, not const
tkurki Apr 16, 2024
17b0f67
move FeatureInfo to server-api, use SignalKApiId
tkurki Apr 16, 2024
645d0a0
remove duplicated type: use FeatureInfo
tkurki Apr 16, 2024
79fc9c8
refactor: rename
tkurki Apr 16, 2024
f2ce485
refactor: remove unused code
tkurki Apr 16, 2024
36a71af
replace shared module const with return value
tkurki Apr 16, 2024
f0732d1
add history apis from the specification
tkurki Apr 16, 2024
9ad53f3
Fix apis not being returned in response.
panaaj Apr 17, 2024
273d2c5
chore: format
panaaj Apr 17, 2024
25e3c49
Update docs
panaaj Apr 18, 2024
df19419
Merge branch 'master' into features_api
panaaj Apr 18, 2024
b236aa5
fix duplicates
panaaj Apr 19, 2024
ed1ef16
update doc
panaaj Apr 19, 2024
458ee4c
Do not return apis when enabled=false.
panaaj Apr 19, 2024
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
11 changes: 11 additions & 0 deletions packages/server-api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,17 @@ export interface ServerAPI extends PluginServerApp {
) => void
}) => void
getSerialPorts: () => Promise<Ports>

getFeatures: () => {
apis: string[]
panaaj marked this conversation as resolved.
Show resolved Hide resolved
plugins: {
id: string
name: string
version: string
enabled: boolean
}
}

getCourse: () => Promise<CourseInfo>
clearDestination: () => Promise<void>
setDestination: (
Expand Down
54 changes: 54 additions & 0 deletions src/api/discovery/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { createDebug } from '../../debug'
const debug = createDebug('signalk-server:api:features')

import { IRouter, Request, Response } from 'express'

import { SignalKMessageHub, WithConfig, WithFeatures } from '../../app'
import { WithSecurityStrategy } from '../../security'

const FEATURES_API_PATH = `/signalk/v2/features`

interface FeaturesApplication
extends IRouter,
WithConfig,
WithFeatures,
WithSecurityStrategy,
SignalKMessageHub {}

interface FeatureInfo {
apis: string[]
plugins: string[]
}

export class FeaturesApi {
private features: FeatureInfo = {
apis: [],
plugins: []
}

constructor(private app: FeaturesApplication) {}

async start() {
return new Promise<void>((resolve) => {
this.initApiRoutes()
resolve()
})
}

private initApiRoutes() {
debug(`** Initialise ${FEATURES_API_PATH} path handlers **`)
// return Feature information
this.app.get(
`${FEATURES_API_PATH}`,
async (req: Request, res: Response) => {
debug(`** GET ${FEATURES_API_PATH}`)
res.json(
await this.app.getFeatures(
typeof req.query.enabled !== 'undefined' ? true : false
)
)
}
)
}
}
143 changes: 139 additions & 4 deletions src/api/discovery/openApi.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,19 @@
},
"servers": [
{
"url": "/"
"url": "/signalk"
}
],
"tags": [
{
"name": "server",
"description": "Signal K Server."
},
{
"name": "features",
"description": "Signal K Server features."
}
],
"tags": [],
"components": {
"schemas": {
"DiscoveryData": {
Expand Down Expand Up @@ -70,6 +79,46 @@
}
}
}
},
"PluginMetaData": {
"type": "object",
"required": ["id", "name", "version"],
"description": "Plugin metadata.",
"properties": {
"id": {
"type": "string",
"description": "Plugin ID."
},
"name": {
"type": "string",
"description": "Plugin name."
},
"version": {
"type": "string",
"description": "Plugin verison."
}
}
},
"FeaturesModel": {
"type": "object",
"required": ["apis", "plugins"],
"description": "Features response",
"properties": {
"apis": {
"type": "array",
"description": "Implemented APIs.",
"items": {
"type": "string"
}
},
"plugins": {
"type": "array",
"description": "Installed Plugins.",
"items": {
"$ref": "#/components/schemas/PluginMetaData"
}
}
}
}
},
"responses": {
Expand All @@ -82,21 +131,107 @@
}
}
}
},
"200Ok": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"state": {
"type": "string",
"enum": ["COMPLETED"]
},
"statusCode": {
"type": "number",
"enum": [200]
}
},
"required": ["state", "statusCode"]
}
}
}
},
"ErrorResponse": {
"description": "Failed operation",
"content": {
"application/json": {
"schema": {
"type": "object",
"description": "Request error response",
"properties": {
"state": {
"type": "string",
"enum": ["FAILED"]
},
"statusCode": {
"type": "number",
"enum": [404]
},
"message": {
"type": "string"
}
},
"required": ["state", "statusCode", "message"]
}
}
}
},
"FeaturesResponse": {
"description": "Server features response.",
"content": {
"application/json": {
"schema": {
"description": "Features response.",
"$ref": "#/components/schemas/FeaturesModel"
}
}
}
}
}
},

"paths": {
"/signalk": {
"/": {
"get": {
"tags": [],
"tags": ["server"],
"summary": "Retrieve server version and service endpoints.",
"description": "Returns data about server's endpoints and versions.",
"responses": {
"200": {
"$ref": "#/components/responses/DiscoveryResponse"
}
}
}
},
"/v2/features": {
"get": {
"tags": ["features"],
"parameters": [
{
"name": "enabled",
"in": "query",
"description": "Limit results to enabled features.",
"required": false,
"explode": false,
"schema": {
"type": "string",
"enum": ["enabled"]
}
}
],
"summary": "Retrieve available server features.",
"description": "Returns object detailing the available server features.",
"responses": {
"200": {
"$ref": "#/components/responses/FeaturesResponse"
},
"default": {
"$ref": "#/components/responses/ErrorResponse"
}
}
}
}
}
}
38 changes: 35 additions & 3 deletions src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { IRouter } from 'express'
import { SignalKMessageHub, WithConfig } from '../app'
import { SignalKMessageHub, WithConfig, WithFeatures } from '../app'
import { WithSecurityStrategy } from '../security'
import { CourseApi } from './course'
import { FeaturesApi } from './discovery'
import { ResourcesApi } from './resources'

export interface ApiResponse {
Expand Down Expand Up @@ -36,14 +37,45 @@ export const Responses = {
}
}

export type SIGNALK_API_ID =
| 'resources'
| 'course'
| 'history'
| 'autopilot'
| 'anchor'
| 'logbook'

export const isSignalKApi = (id: SIGNALK_API_ID): boolean => {
panaaj marked this conversation as resolved.
Show resolved Hide resolved
return [
'resources',
tkurki marked this conversation as resolved.
Show resolved Hide resolved
'course',
'history',
'autopilot',
'anchor',
'logbook'
].includes(id)
}

export const apiList: Array<SIGNALK_API_ID> = []

export const startApis = (
app: SignalKMessageHub & WithSecurityStrategy & IRouter & WithConfig
app: SignalKMessageHub &
WithSecurityStrategy &
IRouter &
WithConfig &
WithFeatures
) => {
const resourcesApi = new ResourcesApi(app)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(app as any).resourcesApi = resourcesApi
apiList.push('resources')

const courseApi = new CourseApi(app, resourcesApi)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(app as any).courseApi = courseApi
Promise.all([resourcesApi.start(), courseApi.start()])
apiList.push('course')

const featuresApi = new FeaturesApi(app)

Promise.all([resourcesApi.start(), courseApi.start(), featuresApi.start()])
}
14 changes: 14 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,17 @@ export interface SelfIdentity {
selfId: string
selfContext: string
}

panaaj marked this conversation as resolved.
Show resolved Hide resolved
interface FeaturesCollection {
apis: string[]
plugins: {
id: string
name: string
version: string
enabled: boolean
}
}

export interface WithFeatures {
getFeatures: (enabledOnly?: boolean) => FeaturesCollection
}
26 changes: 24 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,14 @@ import http from 'http'
import https from 'https'
import _ from 'lodash'
import path from 'path'
import { startApis } from './api'
import { SelfIdentity, ServerApp, SignalKMessageHub, WithConfig } from './app'
import { startApis, apiList, isSignalKApi, SIGNALK_API_ID } from './api'
import {
SelfIdentity,
ServerApp,
SignalKMessageHub,
WithConfig,
WithFeatures
} from './app'
import { ConfigApp, load, sendBaseDeltas } from './config/config'
import { createDebug } from './debug'
import DeltaCache from './deltacache'
Expand Down Expand Up @@ -72,6 +78,7 @@ class Server {
app: ServerApp &
SelfIdentity &
WithConfig &
WithFeatures &
SignalKMessageHub &
PluginManager &
WithSecurityStrategy &
Expand Down Expand Up @@ -116,6 +123,21 @@ class Server {

app.providerStatus = {}

// feature detection
app.getFeatures = async (enabledOnly?: boolean) => {
const pluginApis = app.plugins
.filter((p: { feature: SIGNALK_API_ID }) => {
return p.feature && isSignalKApi(p.feature) ? true : false
})
.map((p: { feature: SIGNALK_API_ID }) => {
return p.feature
})
return {
apis: apiList.slice().concat(pluginApis),
plugins: await app.getPluginsWithFeature(enabledOnly)
}
}

// create first temporary pluginManager to get typechecks, as
// app is any and not typechecked
// TODO separate app.plugins and app.pluginsMap from app
Expand Down
Loading