Feat/js known errors (#1514)

* feat: known errors for js

* chore: add expected exports

* Update js_scripts.rs

* chore: Use agreed upon shape

* chore: add updates to d.ts

* feat: error case

* chore: Add expectedExports as a NameSpace`

* chore: add more documentation to the types.d.ts
This commit is contained in:
J M
2022-06-10 13:04:52 -06:00
committed by GitHub
parent 435956a272
commit 5a88f41718
5 changed files with 1068 additions and 878 deletions

View File

@@ -1,4 +1,7 @@
use std::{path::{PathBuf, Path}, time::Duration};
use std::{
path::{Path, PathBuf},
time::Duration,
};
use models::VolumeId;
use serde::{Deserialize, Serialize};
@@ -12,20 +15,31 @@ use js_engine::{JsExecutionEnvironment, PathForVolumeId};
use super::ProcedureName;
pub use js_engine::{JsError};
pub use js_engine::JsError;
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "kebab-case")]
enum ErrorValue {
Error(String),
Result(serde_json::Value),
}
impl PathForVolumeId for Volumes {
fn path_for(&self, data_dir: &Path, package_id: &PackageId, version: &Version, volume_id: &VolumeId) -> Option<PathBuf> {
fn path_for(
&self,
data_dir: &Path,
package_id: &PackageId,
version: &Version,
volume_id: &VolumeId,
) -> Option<PathBuf> {
let volume = self.get(volume_id)?;
Some(volume.path_for(data_dir, package_id, version, volume_id))
}
fn readonly(&self,volume_id: &VolumeId) -> bool {
fn readonly(&self, volume_id: &VolumeId) -> bool {
self.get(volume_id).map(|x| x.readonly()).unwrap_or(false)
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
@@ -57,12 +71,13 @@ impl JsProcedure {
)
.await?
.run_action(name, input);
let output: O = match timeout {
let output: ErrorValue = match timeout {
Some(timeout_duration) => tokio::time::timeout(timeout_duration, running_action)
.await
.map_err(|_| (JsError::Timeout, "Timed out. Retrying soon...".to_owned()))??,
None => running_action.await?,
};
let output: O = unwrap_known_error(output)?;
Ok(output)
}
.await
@@ -90,12 +105,13 @@ impl JsProcedure {
.await?
.read_only_effects()
.run_action(name, input);
let output: O = match timeout {
let output: ErrorValue = match timeout {
Some(timeout_duration) => tokio::time::timeout(timeout_duration, running_action)
.await
.map_err(|_| (JsError::Timeout, "Timed out. Retrying soon...".to_owned()))??,
None => running_action.await?,
};
let output: O = unwrap_known_error(output)?;
Ok(output)
}
.await
@@ -103,6 +119,28 @@ impl JsProcedure {
}
}
fn unwrap_known_error<O: for<'de> Deserialize<'de>>(
error_value: ErrorValue,
) -> Result<O, (JsError, String)> {
match error_value {
ErrorValue::Error(error) => Err((JsError::Javascript, error)),
ErrorValue::Result(ref value) => match serde_json::from_value(value.clone()) {
Ok(a) => Ok(a),
Err(err) => {
tracing::error!("{}", err);
tracing::debug!("{:?}", err);
Err((
JsError::BoundryLayerSerDe,
format!(
"Couldn't convert output = {:#?} to the correct type",
serde_json::to_string_pretty(&error_value).unwrap_or_default()
),
))
}
},
}
}
#[tokio::test]
async fn js_action_execute() {
let js_action = JsProcedure {};
@@ -157,3 +195,47 @@ async fn js_action_execute() {
)
.unwrap();
}
#[tokio::test]
async fn js_action_execute_error() {
let js_action = JsProcedure {};
let path: PathBuf = "test/js_action_execute/"
.parse::<PathBuf>()
.unwrap()
.canonicalize()
.unwrap();
let package_id = "test-package".parse().unwrap();
let package_version: Version = "0.3.0.3".parse().unwrap();
let name = ProcedureName::SetConfig;
let volumes: Volumes = serde_json::from_value(serde_json::json!({
"main": {
"type": "data"
},
"compat": {
"type": "assets"
},
"filebrowser" :{
"package-id": "filebrowser",
"path": "data",
"readonly": true,
"type": "pointer",
"volume-id": "main",
}
}))
.unwrap();
let input: Option<serde_json::Value> = None;
let timeout = Some(Duration::from_secs(10));
let output: Result<serde_json::Value, _> = js_action
.execute(
&path,
&package_id,
&package_version,
name,
&volumes,
input,
timeout,
)
.await
.unwrap();
assert_eq!("Err((2, \"Not setup\"))", &format!("{:?}", output));
}

View File

@@ -1,7 +0,0 @@
import {Effects, Config, ConfigRes, SetResult, Properties} from './types';
export function properties(effects: Effects): Properties | Promise<Properties>;
export function getConfig(effects: Effects): ConfigRes | Promise<ConfigRes>;
export function setConfig(effects: Effects, input: Config): SetResult | Promise<SetResult>;

View File

@@ -1,266 +0,0 @@
export type Effects = {
writeFile(input: { path: string; volumeId: string; toWrite: string }): Promise<void>;
readFile(input: { volumeId: string; path: string }): Promise<string>;
createDir(input: { volumeId: string; path: string }): Promise<string>;
removeDir(input: { volumeId: string; path: string }): Promise<string>;
removeFile(input: { volumeId: string; path: string }): Promise<void>;
writeJsonFile(input: { volumeId: string; path: string; toWrite: object }): Promise<void>;
readJsonFile(input: { volumeId: string; path: string }): Promise<object>;
trace(whatToPrin: string): void;
warn(whatToPrin: string): void;
error(whatToPrin: string): void;
debug(whatToPrin: string): void;
info(whatToPrin: string): void;
is_sandboxed(): boolean;
};
export type ActionResult = {
version: "0";
message: string;
value?: string;
copyable: boolean;
qr: boolean;
};
export type ConfigRes = {
config?: Config;
spec: ConfigSpec;
};
export type Config = {
[value: string]: any;
};
export type ConfigSpec = {
[value: string]: ValueSpecAny;
};
export type WithDefault<T, Default> = T & {
default?: Default;
};
export type WithDescription<T> = T & {
description?: String;
name: string;
warning?: string;
};
export type ListSpec<T> = {
spec: T;
range: string;
};
export type Tag<T extends string, V> = V & {
type: T;
};
export type Subtype<T extends string, V> = V & {
subtype: T;
};
export type Target<T extends string, V> = V & {
target: T;
};
export type UniqueBy =
| {
any: UniqueBy[];
}
| {
all: UniqueBy[];
}
| string
| null;
export type WithNullable<T> = T & {
nullable: boolean;
};
export type DefaultString =
| String
| {
charset?: string;
len: number;
};
export type ValueSpecString = (
| {}
| {
pattern: string;
"pattern-description": string;
}
) & {
copyable?: boolean;
masked?: boolean;
placeholder?: string;
};
export type ValueSpecNumber = {
range?: string;
integral?: boolean;
units?: string;
placeholder?: number;
};
export type ValueSpecBoolean = {};
export type ValueSpecAny =
| Tag<"boolean", WithDescription<WithDefault<ValueSpecBoolean, boolean>>>
| Tag<"string", WithDescription<WithDefault<WithNullable<ValueSpecString>, DefaultString>>>
| Tag<"number", WithDescription<WithDefault<WithNullable<ValueSpecNumber>, number>>>
| Tag<
"enum",
WithDescription<
WithDefault<
{
values: string[];
"value-names": {
[key: string]: string;
};
},
string
>
>
>
| Tag<"list", ValueSpecList>
| Tag<"object", WithDescription<WithDefault<ValueSpecObject, Config>>>
| Tag<"union", WithDescription<WithDefault<ValueSpecUnion, string>>>
| Tag<
"pointer",
WithDescription<
| Subtype<
"package",
| Target<
"tor-key",
{
"package-id": string;
interface: string;
}
>
| Target<
"tor-address",
{
"package-id": string;
interface: string;
}
>
| Target<
"lan-address",
{
"package-id": string;
interface: string;
}
>
| Target<
"config",
{
"package-id": string;
selector: string;
multi: boolean;
}
>
>
| Subtype<"system", {}>
>
>;
export type ValueSpecUnion = {
tag: {
id: string;
name: string;
description?: string;
"variant-names": {
[key: string]: string;
};
};
variants: {
[key: string]: ConfigSpec;
};
"display-as"?: string;
"unique-by"?: UniqueBy;
};
export type ValueSpecObject = {
spec: ConfigSpec;
"display-as"?: string;
"unique-by"?: UniqueBy;
};
export type ValueSpecList =
| Subtype<"boolean", WithDescription<WithDefault<ListSpec<ValueSpecBoolean>, boolean>>>
| Subtype<"string", WithDescription<WithDefault<ListSpec<ValueSpecString>, string>>>
| Subtype<"number", WithDescription<WithDefault<ListSpec<ValueSpecNumber>, number>>>
| Subtype<
"enum",
WithDescription<
WithDefault<
{
values: string[];
"value-names": {
[key: string]: string;
};
},
string
>
>
>;
export type SetResult = {
signal:
| "SIGTERM"
| "SIGHUP"
| "SIGINT"
| "SIGQUIT"
| "SIGILL"
| "SIGTRAP"
| "SIGABRT"
| "SIGBUS"
| "SIGFPE"
| "SIGKILL"
| "SIGUSR1"
| "SIGSEGV"
| "SIGUSR2"
| "SIGPIPE"
| "SIGALRM"
| "SIGSTKFLT"
| "SIGCHLD"
| "SIGCONT"
| "SIGSTOP"
| "SIGTSTP"
| "SIGTTIN"
| "SIGTTOU"
| "SIGURG"
| "SIGXCPU"
| "SIGXFSZ"
| "SIGVTALRM"
| "SIGPROF"
| "SIGWINCH"
| "SIGIO"
| "SIGPWR"
| "SIGSYS"
| "SIGEMT"
| "SIGINFO";
"depends-on": {
[packageId: string]: string[];
};
};
export type PackagePropertiesV2 = {
[name: string]: PackagePropertyObject | PackagePropertyString;
};
export type PackagePropertyString = {
type: "string";
description?: string;
value: string;
copyable?: boolean;
qr?: boolean;
masked?: boolean;
};
export type PackagePropertyObject = {
value: PackagePropertiesV2;
type: "object";
description: string;
};
export type Properties = {
version: 2;
data: PackagePropertiesV2;
};
export type Dependencies = {
[id: string]: {
check(effects: Effects, input: Config): Promise<void | null>,
autoConfigure(effects: Effects, input: Config): Promise<Config>,
}
}

335
libs/artifacts/types.d.ts vendored Normal file
View File

@@ -0,0 +1,335 @@
export namespace ExpectedExports {
/** Set configuration is called after we have modified and saved the configuration in the embassy ui. Use this to make a file for the docker to read from for configuration. */
export type setConfig = (
effects: Effects,
input: Config,
) => Promise<ResultType<SetResult>>;
/** Get configuration returns a shape that describes the format that the embassy ui will generate, and later send to the set config */
export type getConfig = (effects: Effects) => Promise<ResultType<ConfigRes>>;
/** These are how we make sure the our dependency configurations are valid and if not how to fix them. */
export type dependencies = Dependencies;
/** Properties are used to get values from the docker, like a username + password, what ports we are hosting from */
export type properties = (
effects: Effects,
) => Promise<ResultType<Properties>>;
}
/** Used to reach out from the pure js runtime */
export type Effects = {
/** Usable when not sandboxed */
writeFile(
input: { path: string; volumeId: string; toWrite: string },
): Promise<void>;
readFile(input: { volumeId: string; path: string }): Promise<string>;
/** Create a directory. Usable when not sandboxed */
createDir(input: { volumeId: string; path: string }): Promise<string>;
/** Remove a directory. Usable when not sandboxed */
removeDir(input: { volumeId: string; path: string }): Promise<string>;
removeFile(input: { volumeId: string; path: string }): Promise<void>;
/** Write a json file into an object. Usable when not sandboxed */
writeJsonFile(
input: { volumeId: string; path: string; toWrite: object },
): Promise<void>;
/** Read a json file into an object */
readJsonFile(input: { volumeId: string; path: string }): Promise<object>;
/** Log at the trace level */
trace(whatToPrint: string): void;
/** Log at the warn level */
warn(whatToPrint: string): void;
/** Log at the error level */
error(whatToPrint: string): void;
/** Log at the debug level */
debug(whatToPrint: string): void;
/** Log at the info level */
info(whatToPrint: string): void;
/** Sandbox mode lets us read but not write */
is_sandboxed(): boolean;
};
export type ActionResult = {
version: "0";
message: string;
value?: string;
copyable: boolean;
qr: boolean;
};
export type ConfigRes = {
/** This should be the previous config, that way during set config we start with the previous */
config?: Config;
/** Shape that is describing the form in the ui */
spec: ConfigSpec;
};
export type Config = {
[propertyName: string]: any;
};
export type ConfigSpec = {
/** Given a config value, define what it should render with the following spec */
[configValue: string]: ValueSpecAny;
};
export type WithDefault<T, Default> = T & {
default?: Default;
};
export type WithDescription<T> = T & {
description?: String;
name: string;
warning?: string;
};
export type ListSpec<T> = {
spec: T;
range: string;
};
export type Tag<T extends string, V> = V & {
type: T;
};
export type Subtype<T extends string, V> = V & {
subtype: T;
};
export type Target<T extends string, V> = V & {
target: T;
};
export type UniqueBy =
| {
any: UniqueBy[];
}
| string
| null;
export type WithNullable<T> = T & {
nullable: boolean;
};
export type DefaultString =
| String
| {
/** The chars available for the randome generation */
charset?: string;
/** Length that we generate to */
len: number;
};
export type ValueSpecString =
& (
| {}
| {
pattern: string;
"pattern-description": string;
}
)
& {
copyable?: boolean;
masked?: boolean;
placeholder?: string;
};
export type ValueSpecNumber = {
/** Something like [3,6] or [0, *) */
range?: string;
integral?: boolean;
/** Used a description of the units */
units?: string;
placeholder?: number;
};
export type ValueSpecBoolean = {};
export type ValueSpecAny =
| Tag<"boolean", WithDescription<WithDefault<ValueSpecBoolean, boolean>>>
| Tag<
"string",
WithDescription<WithDefault<WithNullable<ValueSpecString>, DefaultString>>
>
| Tag<
"number",
WithDescription<WithDefault<WithNullable<ValueSpecNumber>, number>>
>
| Tag<
"enum",
WithDescription<
WithDefault<
{
values: string[];
"value-names": {
[key: string]: string;
};
},
string
>
>
>
| Tag<"list", ValueSpecList>
| Tag<"object", WithDescription<WithDefault<ValueSpecObject, Config>>>
| Tag<"union", WithDescription<WithDefault<ValueSpecUnion, string>>>
| Tag<
"pointer",
WithDescription<
| Subtype<
"package",
| Target<
"tor-key",
{
"package-id": string;
interface: string;
}
>
| Target<
"tor-address",
{
"package-id": string;
interface: string;
}
>
| Target<
"lan-address",
{
"package-id": string;
interface: string;
}
>
| Target<
"config",
{
"package-id": string;
selector: string;
multi: boolean;
}
>
>
| Subtype<"system", {}>
>
>;
export type ValueSpecUnion = {
/** What tag for the specification, for tag unions */
tag: {
id: string;
name: string;
description?: string;
"variant-names": {
[key: string]: string;
};
};
/** The possible enum values */
variants: {
[key: string]: ConfigSpec;
};
"display-as"?: string;
"unique-by"?: UniqueBy;
};
export type ValueSpecObject = {
spec: ConfigSpec;
"display-as"?: string;
"unique-by"?: UniqueBy;
};
export type ValueSpecList =
| Subtype<
"boolean",
WithDescription<WithDefault<ListSpec<ValueSpecBoolean>, boolean>>
>
| Subtype<
"string",
WithDescription<WithDefault<ListSpec<ValueSpecString>, string>>
>
| Subtype<
"number",
WithDescription<WithDefault<ListSpec<ValueSpecNumber>, number>>
>
| Subtype<
"enum",
WithDescription<
WithDefault<
{
values: string[];
"value-names": {
[key: string]: string;
};
},
string
>
>
>;
export type SetResult = {
/** These are the unix process signals */
signal:
| "SIGTERM"
| "SIGHUP"
| "SIGINT"
| "SIGQUIT"
| "SIGILL"
| "SIGTRAP"
| "SIGABRT"
| "SIGBUS"
| "SIGFPE"
| "SIGKILL"
| "SIGUSR1"
| "SIGSEGV"
| "SIGUSR2"
| "SIGPIPE"
| "SIGALRM"
| "SIGSTKFLT"
| "SIGCHLD"
| "SIGCONT"
| "SIGSTOP"
| "SIGTSTP"
| "SIGTTIN"
| "SIGTTOU"
| "SIGURG"
| "SIGXCPU"
| "SIGXFSZ"
| "SIGVTALRM"
| "SIGPROF"
| "SIGWINCH"
| "SIGIO"
| "SIGPWR"
| "SIGSYS"
| "SIGEMT"
| "SIGINFO";
"depends-on": {
[packageId: string]: string[];
};
};
export type KnownError = { error: String };
export type ResultType<T> = KnownError | { result: T };
export type PackagePropertiesV2 = {
[name: string]: PackagePropertyObject | PackagePropertyString;
};
export type PackagePropertyString = {
type: "string";
description?: string;
value: string;
/** Let's the ui make this copyable button */
copyable?: boolean;
/** Let the ui create a qr for this field */
qr?: boolean;
/** Hiding the value unless toggled off for field */
masked?: boolean;
};
export type PackagePropertyObject = {
value: PackagePropertiesV2;
type: "object";
description: string;
};
export type Properties = {
version: 2;
data: PackagePropertiesV2;
};
export type Dependencies = {
/** Id is the id of the package, should be the same as the manifest */
[id: string]: {
/** Checks are called to make sure that our dependency is in the correct shape. If a known error is returned we know that the dependency needs modification */
check(effects: Effects, input: Config): Promise<ResultType<void | null>>;
/** This is called after we know that the dependency package needs a new configuration, this would be a transform for defaults */
autoConfigure(effects: Effects, input: Config): Promise<ResultType<Config>>;
};
};