fix: Fix a lint

chore: remove the limit on the long-running

fix: Starting sometimes.

fix: Make it so the stop of the main works

fix: Bind local and tor with package.

wip: envs

fix TS error

import config types from sdk

update package.json
This commit is contained in:
BluJ
2023-02-20 12:22:57 -07:00
committed by Aiden McClelland
parent f5430f9151
commit 5e9e26fa67
34 changed files with 1425 additions and 1195 deletions

View File

@@ -135,7 +135,7 @@ rust-argon2 = "1.0.0"
scopeguard = "1.1" # because avahi-sys fucks your shit up
serde = { version = "1.0.139", features = ["derive", "rc"] }
serde_cbor = { package = "ciborium", version = "0.2.0" }
serde_json = "1.0.82"
serde_json = "1.0.93"
serde_toml = { package = "toml", version = "0.5.9" }
serde_with = { version = "2.0.1", features = ["macros", "json"] }
serde_yaml = "0.9.11"

View File

@@ -101,16 +101,28 @@ async fn create_service_manager(
let mut running_service: Option<NonDetachingJoinHandle<()>> = None;
let seed = seed.clone();
loop {
let current: StartStop = current_state.borrow().clone();
let desired: StartStop = desired_state_receiver.borrow().clone();
let current: StartStop = *current_state.borrow();
let desired: StartStop = *desired_state_receiver.borrow();
match (current, desired) {
(StartStop::Start, StartStop::Start) => (),
(StartStop::Start, StartStop::Stop) => {
if let Err(err) = seed.stop_container().await {
tracing::error!("Could not stop container");
tracing::debug!("{:?}", err)
};
running_service = None;
if persistent_container.is_none() {
if let Err(err) = seed.stop_container().await {
tracing::error!("Could not stop container");
tracing::debug!("{:?}", err)
}
running_service = None;
} else if let Some(current_service) = running_service.take() {
tokio::select! {
_ = current_service => (),
_ = tokio::time::sleep(Duration::from_secs_f64(seed.manifest
.containers
.as_ref()
.and_then(|c| c.main.sigterm_timeout).map(|x| x.as_secs_f64()).unwrap_or_default())) => {
tracing::error!("Could not stop service");
}
}
}
current_state.send(StartStop::Stop).unwrap_or_default();
}
(StartStop::Stop, StartStop::Start) => starting_service(
@@ -123,7 +135,7 @@ async fn create_service_manager(
(StartStop::Stop, StartStop::Stop) => (),
}
if let Err(_) = desired_state_receiver.changed().await {
if desired_state_receiver.changed().await.is_err() {
tracing::error!("Desired state error");
break;
}
@@ -192,10 +204,7 @@ fn starting_service(
current_state.send(StartStop::Start).unwrap_or_default();
})
};
let set_stopped = {
let current_state = current_state.clone();
move || current_state.send(StartStop::Stop)
};
let set_stopped = { move || current_state.send(StartStop::Stop) };
let running_main_loop = async move {
while desired_state.borrow().is_start() {
let result = run_main(

View File

@@ -77,22 +77,25 @@ pub enum BackupReturn {
}
pub struct Gid {
next_gid: watch::Sender<u32>,
main_gid: watch::Sender<ProcessGroupId>,
next_gid: (watch::Sender<u32>, watch::Receiver<u32>),
main_gid: (
watch::Sender<ProcessGroupId>,
watch::Receiver<ProcessGroupId>,
),
}
impl Default for Gid {
fn default() -> Self {
Self {
next_gid: watch::channel(1).0,
main_gid: watch::channel(ProcessGroupId(1)).0,
next_gid: watch::channel(1),
main_gid: watch::channel(ProcessGroupId(1)),
}
}
}
impl Gid {
pub fn new_gid(&self) -> ProcessGroupId {
let mut previous = 0;
self.next_gid.send_modify(|x| {
self.next_gid.0.send_modify(|x| {
previous = *x;
*x = previous + 1;
});
@@ -101,7 +104,7 @@ impl Gid {
pub fn new_main_gid(&self) -> ProcessGroupId {
let gid = self.new_gid();
self.main_gid.send(gid).unwrap();
self.main_gid.0.send(gid).unwrap_or_default();
gid
}
}
@@ -923,7 +926,7 @@ async fn send_signal(manager: &Manager, gid: Arc<Gid>, signal: Signal) -> Result
// .store(false, Ordering::SeqCst);
if let Some(rpc_client) = manager.rpc_client() {
let main_gid = *gid.main_gid.borrow();
let main_gid = *gid.main_gid.0.borrow();
let next_gid = gid.new_gid();
#[cfg(feature = "js_engine")]
if let Err(e) = crate::procedure::js_scripts::JsProcedure::default()

View File

@@ -9,8 +9,8 @@ use tracing::instrument;
use super::manager_seed::ManagerSeed;
use super::{
add_network_for_main, generate_certificate, get_long_running_ip, long_running_docker,
remove_network_for_main, GetRunningIp,
add_network_for_main, get_long_running_ip, long_running_docker, remove_network_for_main,
GetRunningIp,
};
use crate::procedure::docker::DockerContainer;
use crate::util::NonDetachingJoinHandle;
@@ -56,7 +56,8 @@ pub async fn spawn_persistent_container(
let (mut runtime, inserter) =
long_running_docker(&seed, &container).await?;
let ip = match get_long_running_ip(&*seed, &mut runtime).await {
let ip = match get_long_running_ip(&seed, &mut runtime).await {
GetRunningIp::Ip(x) => x,
GetRunningIp::Error(e) => return Err(e),
GetRunningIp::EarlyExit(e) => {
@@ -65,7 +66,7 @@ pub async fn spawn_persistent_container(
return Ok(());
}
};
add_network_for_main(&*seed, ip, generated_certificate).await?;
let svc = add_network_for_main(&seed, ip).await?;
if let Some(inserter_send) = inserter_send.as_mut() {
let _ = inserter_send.send(Arc::new(inserter));
@@ -81,7 +82,7 @@ pub async fn spawn_persistent_container(
a = runtime.running_output => a.map_err(|_| Error::new(eyre!("Manager runtime panicked!"), crate::ErrorKind::Docker)).map(|_| ()),
};
remove_network_for_main(&*seed, ip).await?;
remove_network_for_main(svc).await?;
res
}.await {

View File

@@ -236,7 +236,7 @@ mod tests {
use tokio::sync::watch;
struct OsApiMock {
config_callbacks: watch::Sender<Vec<Callback>>,
config_callbacks: (watch::Sender<Vec<Callback>>, watch::Sender<Vec<Callback>>),
}
impl Default for OsApiMock {
fn default() -> Self {

View File

@@ -93,13 +93,12 @@ impl PackageProcedure {
ErrorKind::NotFound,
)
})?;
let gid;
let rpc_client = man.rpc_client();
if matches!(name, ProcedureName::Main) {
gid = man.gid.new_main_gid();
let gid = if matches!(name, ProcedureName::Main) {
man.gid.new_main_gid()
} else {
gid = man.gid.new_gid();
}
man.gid.new_gid()
};
procedure
.execute(

View File

@@ -234,16 +234,6 @@ impl<R: AsyncRead + AsyncSeek + Unpin + Send + Sync> S9pkReader<R> {
&validated_image_ids,
)?;
#[cfg(feature = "js_engine")]
if man.containers.is_some()
|| matches!(man.main, crate::procedure::PackageProcedure::Script(_))
{
return Err(Error::new(
eyre!("Right now we don't support the containers and the long running main"),
crate::ErrorKind::ValidateS9pk,
));
}
if man.replaces.len() >= MAX_REPLACES {
return Err(Error::new(
eyre!("Cannot have more than {MAX_REPLACES} replaces"),

File diff suppressed because it is too large Load Diff

View File

@@ -74,6 +74,7 @@
"patch-db-client": "file: ../../../patch-db/client",
"pbkdf2": "^3.1.2",
"rxjs": "^7.5.6",
"start-sdk": "^0.4.0-lib0.alpha3",
"swiper": "^8.2.4",
"ts-matches": "^5.2.1",
"tslib": "^2.3.0",
@@ -103,7 +104,7 @@
"raw-loader": "^4.0.2",
"ts-node": "^10.7.0",
"tslint": "^6.1.3",
"typescript": "^4.6.3",
"typescript": "^4.8.4",
"webpack-bundle-analyzer": "^4.8.0"
},
"husky": {

View File

@@ -12,7 +12,7 @@ import {
ModalController,
} from '@ionic/angular'
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
import { ConfigSpec } from 'src/app/pkg-config/config-types'
import { ConfigSpec } from 'start-sdk/types/config-types'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ErrorToastService } from '@start9labs/shared'
import { MappedBackupTarget } from 'src/app/types/mapped-backup-target'
@@ -285,31 +285,47 @@ const CifsSpec: ConfigSpec = {
'pattern-description': `Must be a valid hostname. e.g. 'My Computer' OR 'my-computer.local'`,
nullable: false,
masked: false,
copyable: false,
default: null,
textarea: false,
warning: null,
},
path: {
type: 'string',
name: 'Path',
description: `On Windows, this is the fully qualified path to the shared folder, (e.g. /Desktop/my-folder).\n\n On Linux and Mac, this is the literal name of the shared folder (e.g. my-shared-folder).`,
placeholder: 'e.g. my-shared-folder or /Desktop/my-folder',
pattern: null,
'pattern-description': null,
nullable: false,
masked: false,
copyable: false,
default: null,
textarea: false,
warning: null,
},
username: {
type: 'string',
name: 'Username',
description: `On Linux, this is the samba username you created when sharing the folder.\n\n On Mac and Windows, this is the username of the user who is sharing the folder.`,
placeholder: null,
pattern: null,
'pattern-description': null,
nullable: false,
masked: false,
copyable: false,
default: null,
textarea: false,
warning: null,
},
password: {
type: 'string',
name: 'Password',
description: `On Linux, this is the samba password you created when sharing the folder.\n\n On Mac and Windows, this is the password of the user who is sharing the folder.`,
placeholder: null,
pattern: null,
'pattern-description': null,
nullable: true,
masked: true,
copyable: false,
default: null,
textarea: false,
warning: null,
},
}

View File

@@ -235,6 +235,7 @@
name:
objectListDisplay[entry.key][i].displayAs ||
'Entry ' + (i + 1),
description: null,
new: false,
edited: abstractControl.dirty
}"

View File

@@ -19,7 +19,7 @@ import {
ValueSpecList,
ValueSpecListOf,
ValueSpecUnion,
} from 'src/app/pkg-config/config-types'
} from 'start-sdk/types/config-types'
import { FormService } from 'src/app/services/form.service'
import { EnumListPage } from 'src/app/modals/enum-list/enum-list.page'
import { THEME, pauseFor } from '@start9labs/shared'
@@ -249,7 +249,7 @@ export class FormObjectComponent {
const alert = await this.alertCtrl.create({
header: name,
message: description,
message: description || '',
buttons: [
{
text: 'OK',
@@ -395,7 +395,7 @@ export class FormLabelComponent {
name: string
new: boolean
edited: boolean
description?: string
description: string | null
required?: boolean
newOptions?: boolean
}
@@ -408,7 +408,7 @@ export class FormLabelComponent {
const alert = await this.alertCtrl.create({
header: name,
message: description,
message: description || '',
buttons: [
{
text: 'OK',

View File

@@ -6,8 +6,8 @@ import {
ValidationErrors,
} from '@angular/forms'
import { IonicSafeString } from '@ionic/angular'
import { ListValueSpecOf } from 'src/app/pkg-config/config-types'
import { Range } from 'src/app/pkg-config/config-utilities'
import { ListValueSpecOf } from 'start-sdk/types/config-types'
import { Range } from 'src/app/util/config-utilities'
import { getElementId } from './form-object.component'
@Pipe({
@@ -50,7 +50,7 @@ export class ToEnumListDisplayPipe implements PipeTransform {
name: 'toWarningText',
})
export class ToWarningTextPipe implements PipeTransform {
transform(text?: string): IonicSafeString | string {
transform(text: string | null): IonicSafeString | string {
return text
? new IonicSafeString(`<ion-text color="warning">${text}</ion-text>`)
: ''

View File

@@ -13,7 +13,7 @@ import {
isObject,
} from '@start9labs/shared'
import { DependentInfo } from 'src/app/types/dependent-info'
import { ConfigSpec } from 'src/app/pkg-config/config-types'
import { ConfigSpec } from 'start-sdk/types/config-types'
import {
DataModel,
PackageDataEntry,
@@ -100,13 +100,11 @@ export class AppConfigPage {
}
this.configForm = this.formService.createForm(
this.configSpec,
this.configSpec!,
newConfig || this.original,
)
this.hasOptions = !!Object.values(this.configSpec).find(
valSpec => valSpec.type !== 'pointer',
)
this.hasOptions = false
if (patch) {
this.diff = this.getDiff(patch)

View File

@@ -1,6 +1,6 @@
import { Component, Input } from '@angular/core'
import { ModalController } from '@ionic/angular'
import { ValueSpecListOf } from 'src/app/pkg-config/config-types'
import { ValueSpecListOf } from 'start-sdk/types/config-types'
@Component({
selector: 'enum-list',

View File

@@ -5,7 +5,7 @@ import {
convertValuesRecursive,
FormService,
} from 'src/app/services/form.service'
import { ConfigSpec } from 'src/app/pkg-config/config-types'
import { ConfigSpec } from 'start-sdk/types/config-types'
export interface ActionButton {
text: string

View File

@@ -14,7 +14,7 @@ import { ActionSheetButton } from '@ionic/core'
import { ErrorToastService, sameUrl, toUrl } from '@start9labs/shared'
import { AbstractMarketplaceService } from '@start9labs/marketplace'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ValueSpecObject } from 'src/app/pkg-config/config-types'
import { ValueSpecObject } from 'start-sdk/types/config-types'
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
import { PatchDB } from 'patch-db-client'
import { DataModel, UIStore } from 'src/app/services/patch-db/data-model'
@@ -259,6 +259,8 @@ function getMarketplaceValueSpec(): ValueSpecObject {
return {
type: 'object',
name: 'Add Custom Registry',
description: null,
warning: null,
spec: {
url: {
type: 'string',
@@ -266,10 +268,12 @@ function getMarketplaceValueSpec(): ValueSpecObject {
description: 'A fully-qualified URL of the custom registry',
nullable: false,
masked: false,
copyable: false,
pattern: `https?:\/\/[a-zA-Z0-9][a-zA-Z0-9-\.]+[a-zA-Z0-9]\.[^\s]{2,}`,
'pattern-description': 'Must be a valid URL',
placeholder: 'e.g. https://example.org',
default: null,
textarea: false,
warning: null,
},
},
}

View File

@@ -12,7 +12,7 @@ import {
} from 'src/app/modals/generic-input/generic-input.component'
import { PatchDB } from 'patch-db-client'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ConfigSpec } from 'src/app/pkg-config/config-types'
import { ConfigSpec } from 'start-sdk/types/config-types'
import * as yaml from 'js-yaml'
import { v4 } from 'uuid'
import { DataModel, DevData } from 'src/app/services/patch-db/data-model'
@@ -223,12 +223,14 @@ const SAMPLE_CONFIG: ConfigSpec = {
name: 'Example String Input',
nullable: false,
masked: false,
copyable: false,
// optional
description: 'Example description for required string input.',
placeholder: 'Enter string value',
pattern: '^[a-zA-Z0-9! _]+$',
'pattern-description': 'Must be alphanumeric (may contain underscore).',
default: null,
textarea: false,
warning: null,
},
'sample-number': {
type: 'number',
@@ -241,6 +243,7 @@ const SAMPLE_CONFIG: ConfigSpec = {
units: 'ms',
description: 'Example description for optional number input.',
placeholder: 'Enter number value',
default: null,
},
'sample-boolean': {
type: 'boolean',
@@ -248,6 +251,7 @@ const SAMPLE_CONFIG: ConfigSpec = {
// optional
description: 'Example description for boolean toggle',
default: true,
warning: null,
},
'sample-enum': {
type: 'enum',

View File

@@ -1,4 +1,4 @@
import { ConfigSpec } from 'src/app/pkg-config/config-types'
import { ConfigSpec } from 'start-sdk/types/config-types'
import { DevProjectData } from 'src/app/services/patch-db/data-model'
export type BasicInfo = {
@@ -27,10 +27,11 @@ export function getBasicInfoSpec(devData: DevProjectData): ConfigSpec {
placeholder: 'e.g. bitcoind',
nullable: false,
masked: false,
copyable: false,
pattern: '^([a-z][a-z0-9]*)(-[a-z0-9]+)*$',
'pattern-description': 'Must be kebab case',
default: basicInfo?.id,
default: basicInfo?.id || '',
textarea: false,
warning: null,
},
title: {
type: 'string',
@@ -39,8 +40,11 @@ export function getBasicInfoSpec(devData: DevProjectData): ConfigSpec {
placeholder: 'e.g. Bitcoin Core',
nullable: false,
masked: false,
copyable: false,
pattern: null,
'pattern-description': null,
default: basicInfo ? basicInfo.title : devData.name,
textarea: false,
warning: null,
},
'service-version-number': {
type: 'string',
@@ -50,39 +54,44 @@ export function getBasicInfoSpec(devData: DevProjectData): ConfigSpec {
placeholder: 'e.g. 0.1.2.3',
nullable: false,
masked: false,
copyable: false,
pattern: '^([0-9]+).([0-9]+).([0-9]+).([0-9]+)$',
'pattern-description': 'Must be valid Emver version',
default: basicInfo?.['service-version-number'],
default: basicInfo?.['service-version-number'] || '',
textarea: false,
warning: null,
},
description: {
type: 'object',
name: 'Marketplace Descriptions',
description: null,
warning: null,
spec: {
short: {
type: 'string',
name: 'Short Description',
description:
'This is the first description visible to the user in the marketplace',
placeholder: null,
nullable: false,
masked: false,
copyable: false,
textarea: true,
default: basicInfo?.description?.short,
default: basicInfo?.description?.short || '',
pattern: '^.{1,320}$',
'pattern-description': 'Must be shorter than 320 characters',
warning: null,
},
long: {
type: 'string',
name: 'Long Description',
description: `This description will display with additional details in the service's individual marketplace page`,
placeholder: null,
nullable: false,
masked: false,
copyable: false,
textarea: true,
default: basicInfo?.description?.long,
default: basicInfo?.description?.long || '',
pattern: '^.{1,5000}$',
'pattern-description': 'Must be shorter than 5000 characters',
warning: null,
},
},
},
@@ -94,13 +103,16 @@ export function getBasicInfoSpec(devData: DevProjectData): ConfigSpec {
placeholder: 'e.g. Markdown _release notes_ for **Bitcoin Core**',
nullable: false,
masked: false,
copyable: false,
pattern: null,
'pattern-description': null,
textarea: true,
default: basicInfo?.['release-notes'],
default: basicInfo?.['release-notes'] || '',
warning: null,
},
license: {
type: 'enum',
name: 'License',
warning: null,
values: [
'gnu-agpl-v3',
'gnu-gpl-v3',
@@ -132,40 +144,52 @@ export function getBasicInfoSpec(devData: DevProjectData): ConfigSpec {
description:
'The Start9 wrapper repository URL for the package. This repo contains the manifest file (this), any scripts necessary for configuration, backups, actions, or health checks',
placeholder: 'e.g. www.github.com/example',
pattern: null,
'pattern-description': null,
nullable: false,
masked: false,
copyable: false,
default: basicInfo?.['wrapper-repo'],
default: basicInfo?.['wrapper-repo'] || '',
textarea: false,
warning: null,
},
'upstream-repo': {
type: 'string',
name: 'Upstream Repo',
description: 'The original project repository URL',
placeholder: 'e.g. www.github.com/example',
pattern: null,
'pattern-description': null,
nullable: true,
masked: false,
copyable: false,
default: basicInfo?.['upstream-repo'],
default: basicInfo?.['upstream-repo'] || '',
textarea: false,
warning: null,
},
'support-site': {
type: 'string',
name: 'Support Site',
description: 'URL to the support site / channel for the project',
placeholder: 'e.g. start9.com/support',
pattern: null,
'pattern-description': null,
nullable: true,
masked: false,
copyable: false,
default: basicInfo?.['support-site'],
default: basicInfo?.['support-site'] || '',
textarea: false,
warning: null,
},
'marketing-site': {
type: 'string',
name: 'Marketing Site',
description: 'URL to the marketing site / channel for the project',
placeholder: 'e.g. start9.com',
pattern: null,
'pattern-description': null,
nullable: true,
masked: false,
copyable: false,
default: basicInfo?.['marketing-site'],
default: basicInfo?.['marketing-site'] || '',
textarea: false,
warning: null,
},
}
}

View File

@@ -94,6 +94,7 @@ export class SideloadPage {
const guid = await this.api.sideloadPackage({
manifest: this.toUpload.manifest!,
icon: this.toUpload.icon!,
size: this.toUpload.file!.size,
})
this.api
.uploadPackage(guid, this.toUpload.file!)

View File

@@ -9,7 +9,7 @@ import {
import { AlertInput } from '@ionic/core'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ActionSheetButton } from '@ionic/core'
import { ValueSpecObject } from 'src/app/pkg-config/config-types'
import { ValueSpecObject } from 'start-sdk/types/config-types'
import { RR } from 'src/app/services/api/api.types'
import { pauseFor, ErrorToastService } from '@start9labs/shared'
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
@@ -345,23 +345,33 @@ function getWifiValueSpec(
name: 'WiFi Credentials',
description:
'Enter the network SSID and password. You can connect now or save the network for later.',
warning: null,
spec: {
ssid: {
type: 'string',
name: 'Network SSID',
description: null,
placeholder: null,
pattern: null,
'pattern-description': null,
nullable: false,
masked: false,
copyable: false,
default: ssid,
default: ssid || null,
textarea: false,
warning: null,
},
password: {
type: 'string',
name: 'Password',
description: null,
placeholder: null,
nullable: !needsPW,
masked: true,
copyable: false,
pattern: '^.{8,}$',
'pattern-description': 'Must be longer than 8 characters',
default: null,
textarea: false,
warning: null,
},
},
}

View File

@@ -1,158 +0,0 @@
export type ConfigSpec = Record<string, ValueSpec>
export type ValueType =
| 'string'
| 'number'
| 'boolean'
| 'enum'
| 'list'
| 'object'
| 'union'
export type ValueSpec = ValueSpecOf<ValueType>
// core spec types. These types provide the metadata for performing validations
export type ValueSpecOf<T extends ValueType> = T extends 'string'
? ValueSpecString
: T extends 'number'
? ValueSpecNumber
: T extends 'boolean'
? ValueSpecBoolean
: T extends 'enum'
? ValueSpecEnum
: T extends 'list'
? ValueSpecList
: T extends 'object'
? ValueSpecObject
: T extends 'union'
? ValueSpecUnion
: never
export interface ValueSpecString extends ListValueSpecString, WithStandalone {
type: 'string'
default?: DefaultString
nullable: boolean
textarea?: boolean
}
export interface ValueSpecNumber extends ListValueSpecNumber, WithStandalone {
type: 'number'
nullable: boolean
default?: number
}
export interface ValueSpecEnum extends ListValueSpecEnum, WithStandalone {
type: 'enum'
default: string
}
export interface ValueSpecBoolean extends WithStandalone {
type: 'boolean'
default: boolean
}
export interface ValueSpecUnion {
type: 'union'
tag: UnionTagSpec
variants: { [key: string]: ConfigSpec }
default: string
}
export interface ValueSpecObject extends WithStandalone {
type: 'object'
spec: ConfigSpec
}
export interface WithStandalone {
name: string
description?: string
warning?: string
}
// no lists of booleans or lists
export type ListValueSpecType =
| 'string'
| 'number'
| 'enum'
| 'object'
| 'union'
// represents a spec for the values of a list
export type ListValueSpecOf<T extends ListValueSpecType> = T extends 'string'
? ListValueSpecString
: T extends 'number'
? ListValueSpecNumber
: T extends 'enum'
? ListValueSpecEnum
: T extends 'object'
? ListValueSpecObject
: T extends 'union'
? ListValueSpecUnion
: never
// represents a spec for a list
export type ValueSpecList = ValueSpecListOf<ListValueSpecType>
export interface ValueSpecListOf<T extends ListValueSpecType>
extends WithStandalone {
type: 'list'
subtype: T
spec: ListValueSpecOf<T>
range: string // '[0,1]' (inclusive) OR '[0,*)' (right unbounded), normal math rules
default: string[] | number[] | DefaultString[] | object[]
}
// sometimes the type checker needs just a little bit of help
export function isValueSpecListOf<S extends ListValueSpecType>(
t: ValueSpecList,
s: S,
): t is ValueSpecListOf<S> {
return t.subtype === s
}
export interface ListValueSpecString {
pattern?: string
'pattern-description'?: string
masked: boolean
copyable: boolean
placeholder?: string
}
export interface ListValueSpecNumber {
range: string
integral: boolean
units?: string
placeholder?: string
}
export interface ListValueSpecEnum {
values: string[]
'value-names': { [value: string]: string }
}
export interface ListValueSpecObject {
spec: ConfigSpec // this is a mapped type of the config object at this level, replacing the object's values with specs on those values
'unique-by': UniqueBy // indicates whether duplicates can be permitted in the list
'display-as'?: string // this should be a handlebars template which can make use of the entire config which corresponds to 'spec'
}
export type UniqueBy = null | string | { any: UniqueBy[] } | { all: UniqueBy[] }
export interface ListValueSpecUnion {
tag: UnionTagSpec
variants: { [key: string]: ConfigSpec }
'display-as'?: string // this may be a handlebars template which can conditionally (on tag.id) make use of each union's entries, or if left blank will display as tag.id
'unique-by': UniqueBy
default: string // this should be the variantName which one prefers a user to start with by default when creating a new union instance in a list
}
export interface UnionTagSpec {
id: string // The name of the field containing one of the union variants
'variant-names': {
// the name of each variant
[variant: string]: string
}
name: string
description?: string
warning?: string
}
export type DefaultString = string | { charset: string; len: number }

View File

@@ -7,8 +7,7 @@ import {
PackageState,
ServerStatusInfo,
} from 'src/app/services/patch-db/data-model'
import { Metric, NotificationLevel, RR, ServerNotifications } from './api.types'
import { Metric, RR, NotificationLevel, ServerNotifications } from './api.types'
import { BTC_ICON, LND_ICON, PROXY_ICON } from './api-icons'
import { DependencyMetadata, MarketplacePkg } from '@start9labs/marketplace'
import { Log } from '@start9labs/shared'
@@ -180,11 +179,14 @@ export module Mock {
type: 'string',
name: 'Re-sync Reason',
description: 'Your reason for re-syncing. Why are you doing this?',
placeholder: null,
nullable: false,
masked: false,
copyable: false,
pattern: '^[a-zA-Z]+$',
'pattern-description': 'Must contain only letters.',
textarea: false,
warning: null,
default: null,
},
name: {
type: 'string',
@@ -192,14 +194,19 @@ export module Mock {
description: 'Tell the class your name.',
nullable: true,
masked: false,
copyable: false,
warning: 'You may loose all your money by providing your name.',
placeholder: null,
pattern: null,
'pattern-description': null,
textarea: false,
default: null,
},
notifications: {
name: 'Notification Preferences',
type: 'list',
subtype: 'enum',
description: 'how you want to be notified',
warning: null,
range: '[1,3]',
default: ['email'],
spec: {
@@ -221,6 +228,9 @@ export module Mock {
default: 100,
range: '[0, 9999]',
integral: true,
units: null,
placeholder: null,
warning: null,
},
'top-speed': {
type: 'number',
@@ -230,6 +240,9 @@ export module Mock {
range: '[-1000, 1000]',
integral: false,
units: 'm/s',
placeholder: null,
warning: null,
default: null,
},
testnet: {
name: 'Testnet',
@@ -257,22 +270,33 @@ export module Mock {
name: 'Emergency Contact',
type: 'object',
description: 'The person to contact in case of emergency.',
warning: null,
spec: {
name: {
type: 'string',
name: 'Name',
description: null,
nullable: false,
masked: false,
copyable: false,
pattern: '^[a-zA-Z]+$',
'pattern-description': 'Must contain only letters.',
placeholder: null,
textarea: false,
warning: null,
default: null,
},
email: {
type: 'string',
name: 'Email',
description: null,
nullable: false,
masked: false,
copyable: true,
placeholder: null,
pattern: null,
'pattern-description': null,
textarea: false,
warning: null,
default: null,
},
},
},
@@ -287,10 +311,10 @@ export module Mock {
range: '[1,10]',
default: ['192.168.1.1'],
spec: {
placeholder: null,
pattern: '^[0-9]{1,3}([,.][0-9]{1,3})?$',
'pattern-description': 'Must be a valid IP address',
masked: false,
copyable: false,
},
},
bitcoinNode: {
@@ -314,7 +338,12 @@ export module Mock {
description: 'the lan address',
nullable: true,
masked: false,
copyable: false,
placeholder: null,
pattern: null,
'pattern-description': null,
textarea: false,
warning: null,
default: null,
},
},
external: {
@@ -327,7 +356,9 @@ export module Mock {
pattern: '.*',
'pattern-description': 'anything',
masked: false,
copyable: true,
placeholder: null,
textarea: false,
warning: null,
},
},
},
@@ -1281,6 +1312,7 @@ export module Mock {
type: 'list',
subtype: 'object',
description: 'This is a list of objects, like users or something',
warning: null,
range: '[0,4]',
default: [
{
@@ -1307,7 +1339,12 @@ export module Mock {
description: 'User first name',
nullable: true,
masked: false,
copyable: false,
placeholder: null,
pattern: null,
'pattern-description': null,
textarea: false,
warning: null,
default: null,
},
'last-name': {
name: 'Last Name',
@@ -1321,7 +1358,9 @@ export module Mock {
pattern: '^[a-zA-Z]+$',
'pattern-description': 'must contain only letters.',
masked: false,
copyable: true,
placeholder: null,
textarea: false,
warning: null,
},
age: {
name: 'Age',
@@ -1331,6 +1370,9 @@ export module Mock {
integral: false,
warning: 'User must be at least 18.',
range: '[18,*)',
units: null,
placeholder: null,
default: null,
},
},
},
@@ -1347,6 +1389,8 @@ export module Mock {
spec: {
tag: {
id: 'preference',
description: null,
warning: null,
'variant-names': {
summer: 'Summer',
winter: 'Winter',
@@ -1354,6 +1398,7 @@ export module Mock {
},
name: 'Preference',
},
'display-as': null,
// this default is used to make a union selection when a new list element is first created
default: 'summer',
variants: {
@@ -1365,12 +1410,17 @@ export module Mock {
description: 'What is your favorite tree?',
default: 'Maple',
masked: false,
copyable: false,
placeholder: null,
pattern: null,
'pattern-description': null,
textarea: false,
warning: null,
},
'favorite-flower': {
name: 'Favorite Flower',
type: 'enum',
description: 'Select your favorite flower',
warning: null,
'value-names': {
none: 'Hate Flowers',
red: 'Red',
@@ -1386,6 +1436,7 @@ export module Mock {
name: 'Like Snow?',
type: 'boolean',
description: 'Do you like snow or not?',
warning: null,
default: true,
},
},
@@ -1412,6 +1463,7 @@ export module Mock {
type: 'number',
integral: false,
description: 'Your favorite number of all time',
placeholder: null,
warning:
'Once you set this number, it can never be changed without severe consequences.',
nullable: true,
@@ -1424,9 +1476,12 @@ export module Mock {
type: 'list',
subtype: 'number',
description: 'Numbers that you like but are not your top favorite.',
warning: null,
spec: {
integral: false,
range: '[-100,200)',
units: null,
placeholder: null,
},
range: '[0,10]',
default: [2, 3],
@@ -1441,6 +1496,7 @@ export module Mock {
name: 'Laws',
type: 'object',
description: 'the law of the realm',
warning: null,
spec: {
law1: {
name: 'First Law',
@@ -1448,7 +1504,12 @@ export module Mock {
description: 'the first law',
nullable: true,
masked: false,
copyable: true,
placeholder: null,
pattern: null,
'pattern-description': null,
textarea: false,
warning: null,
default: null,
},
law2: {
name: 'Second Law',
@@ -1456,7 +1517,12 @@ export module Mock {
description: 'the second law',
nullable: true,
masked: false,
copyable: true,
placeholder: null,
pattern: null,
'pattern-description': null,
textarea: false,
warning: null,
default: null,
},
},
},
@@ -1465,10 +1531,12 @@ export module Mock {
type: 'list',
subtype: 'object',
description: 'the people who make the rules',
warning: null,
range: '[0,2]',
default: [],
spec: {
'unique-by': null,
'display-as': null,
spec: {
rulemakername: {
name: 'Rulemaker Name',
@@ -1480,7 +1548,11 @@ export module Mock {
len: 12,
},
masked: false,
copyable: false,
placeholder: null,
pattern: null,
'pattern-description': null,
textarea: false,
warning: null,
},
rulemakerip: {
name: 'Rulemaker IP',
@@ -1492,7 +1564,9 @@ export module Mock {
'^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$',
'pattern-description': 'may only contain numbers and periods',
masked: false,
copyable: true,
placeholder: null,
textarea: false,
warning: null,
},
},
},
@@ -1506,7 +1580,9 @@ export module Mock {
pattern: '^[a-zA-Z]+$',
'pattern-description': 'must contain only letters.',
masked: false,
copyable: true,
placeholder: null,
textarea: false,
warning: null,
},
rpcpass: {
name: 'RPC User Password',
@@ -1518,7 +1594,11 @@ export module Mock {
len: 20,
},
masked: true,
copyable: true,
placeholder: null,
pattern: null,
'pattern-description': null,
textarea: false,
warning: null,
},
},
},
@@ -1570,7 +1650,9 @@ export module Mock {
pattern: '.*',
'pattern-description': 'anything',
masked: false,
copyable: true,
placeholder: null,
textarea: false,
warning: null,
},
'private-domain': {
name: 'Private Domain',
@@ -1578,7 +1660,12 @@ export module Mock {
description: 'the private address of the node',
nullable: false,
masked: true,
copyable: true,
placeholder: null,
pattern: null,
'pattern-description': null,
textarea: false,
warning: null,
default: null,
},
},
},
@@ -1589,9 +1676,12 @@ export module Mock {
integral: true,
description:
'the default port for your Bitcoin node. default: 8333, testnet: 18333, regtest: 18444',
warning: null,
nullable: false,
default: 8333,
range: '(0, 9998]',
units: null,
placeholder: null,
},
'favorite-slogan': {
name: 'Favorite Slogan',
@@ -1600,7 +1690,12 @@ export module Mock {
'You most favorite slogan in the whole world, used for paying you.',
nullable: true,
masked: true,
copyable: true,
placeholder: null,
pattern: null,
'pattern-description': null,
textarea: false,
warning: null,
default: null,
},
rpcallowip: {
name: 'RPC Allowed IPs',
@@ -1614,7 +1709,7 @@ export module Mock {
default: ['192.168.1.1'],
spec: {
masked: false,
copyable: false,
placeholder: null,
pattern:
'((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|((^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$)|(^[a-z2-7]{16}\\.onion$)|(^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$))',
'pattern-description': 'must be a valid ipv4, ipv6, or domain name',
@@ -1625,23 +1720,28 @@ export module Mock {
type: 'list',
subtype: 'string',
description: 'api keys that are authorized to access your Bitcoin node.',
warning: null,
range: '[0,*)',
default: [],
spec: {
masked: false,
copyable: false,
placeholder: null,
pattern: null,
'pattern-description': null,
},
},
'more-advanced': {
name: 'More Advanced',
type: 'object',
description: 'Advanced settings',
warning: null,
spec: {
notifications: {
name: 'Notification Preferences',
type: 'list',
subtype: 'enum',
description: 'how you want to be notified',
warning: null,
range: '[1,3]',
default: ['email'],
spec: {
@@ -1666,6 +1766,7 @@ export module Mock {
name: 'Laws',
type: 'object',
description: 'the law of the realm',
warning: null,
spec: {
law1: {
name: 'First Law',
@@ -1673,7 +1774,12 @@ export module Mock {
description: 'the first law',
nullable: true,
masked: false,
copyable: true,
placeholder: null,
pattern: null,
'pattern-description': null,
textarea: false,
warning: null,
default: null,
},
law2: {
name: 'Second Law',
@@ -1681,7 +1787,12 @@ export module Mock {
description: 'the second law',
nullable: true,
masked: false,
copyable: true,
placeholder: null,
pattern: null,
'pattern-description': null,
textarea: false,
warning: null,
default: null,
},
law4: {
name: 'Fourth Law',
@@ -1689,17 +1800,24 @@ export module Mock {
description: 'the fourth law',
nullable: true,
masked: false,
copyable: true,
placeholder: null,
pattern: null,
'pattern-description': null,
textarea: false,
warning: null,
default: null,
},
law3: {
name: 'Third Law',
type: 'list',
subtype: 'object',
description: 'the third law',
warning: null,
range: '[0,2]',
default: [],
spec: {
'unique-by': null,
'display-as': null,
spec: {
lawname: {
name: 'Law Name',
@@ -1711,7 +1829,11 @@ export module Mock {
len: 12,
},
masked: false,
copyable: false,
placeholder: null,
pattern: null,
'pattern-description': null,
textarea: false,
warning: null,
},
lawagency: {
name: 'Law agency',
@@ -1724,7 +1846,9 @@ export module Mock {
'pattern-description':
'may only contain numbers and periods',
masked: false,
copyable: true,
placeholder: null,
textarea: false,
warning: null,
},
},
},
@@ -1735,7 +1859,12 @@ export module Mock {
description: 'the fifth law',
nullable: true,
masked: false,
copyable: true,
placeholder: null,
pattern: null,
'pattern-description': null,
textarea: false,
warning: null,
default: null,
},
},
},
@@ -1744,10 +1873,12 @@ export module Mock {
type: 'list',
subtype: 'object',
description: 'the people who make the rules',
warning: null,
range: '[0,2]',
default: [],
spec: {
'unique-by': null,
'display-as': null,
spec: {
rulemakername: {
name: 'Rulemaker Name',
@@ -1759,7 +1890,11 @@ export module Mock {
len: 12,
},
masked: false,
copyable: false,
placeholder: null,
pattern: null,
'pattern-description': null,
textarea: false,
warning: null,
},
rulemakerip: {
name: 'Rulemaker IP',
@@ -1772,7 +1907,9 @@ export module Mock {
'pattern-description':
'may only contain numbers and periods',
masked: false,
copyable: true,
placeholder: null,
textarea: false,
warning: null,
},
},
},
@@ -1786,7 +1923,9 @@ export module Mock {
pattern: '^[a-zA-Z]+$',
'pattern-description': 'must contain only letters.',
masked: false,
copyable: true,
placeholder: null,
textarea: false,
warning: null,
},
rpcpass: {
name: 'RPC User Password',
@@ -1798,7 +1937,11 @@ export module Mock {
len: 20,
},
masked: true,
copyable: true,
placeholder: null,
pattern: null,
'pattern-description': null,
textarea: false,
warning: null,
},
},
},

View File

@@ -1,7 +1,7 @@
import { Dump, Revision } from 'patch-db-client'
import { MarketplacePkg, StoreInfo } from '@start9labs/marketplace'
import { PackagePropertiesVersioned } from 'src/app/util/properties.util'
import { ConfigSpec } from 'src/app/pkg-config/config-types'
import { ConfigSpec } from 'start-sdk/types/config-types'
import {
DataModel,
DependencyError,
@@ -253,6 +253,7 @@ export module RR {
export type SideloadPackageReq = {
manifest: Manifest
icon: string // base64
size: number // bytes
}
export type SideloadPacakgeRes = string //guid

View File

@@ -23,8 +23,8 @@ import {
ValueSpecObject,
ValueSpecString,
ValueSpecUnion,
} from 'src/app/pkg-config/config-types'
import { getDefaultString, Range } from '../pkg-config/config-utilities'
} from 'start-sdk/types/config-types'
import { getDefaultString, Range } from '../util/config-utilities'
const Mustache = require('mustache')
@Injectable({
@@ -360,7 +360,7 @@ function listObjEquals(
val1: any,
val2: any,
): boolean {
if (uniqueBy === null) {
if (!uniqueBy) {
return false
} else if (typeof uniqueBy === 'string') {
return itemEquals(spec.spec[uniqueBy], val1[uniqueBy], val2[uniqueBy])
@@ -388,7 +388,7 @@ function objEquals(
val1: any,
val2: any,
): boolean {
if (uniqueBy === null) {
if (!uniqueBy) {
return false
} else if (typeof uniqueBy === 'string') {
// TODO: fix types
@@ -419,7 +419,7 @@ function unionEquals(
): boolean {
const tagId = spec.tag.id
const variant = spec.variants[val1[tagId]]
if (uniqueBy === null) {
if (!uniqueBy) {
return false
} else if (typeof uniqueBy === 'string') {
if (uniqueBy === tagId) {
@@ -474,7 +474,7 @@ function uniqueByMessage(
): string {
let joinFunc
const subSpecs: string[] = []
if (uniqueBy === null) {
if (!uniqueBy) {
return ''
} else if (typeof uniqueBy === 'string') {
return configSpec[uniqueBy]

View File

@@ -1,4 +1,4 @@
import { ConfigSpec } from 'src/app/pkg-config/config-types'
import { ConfigSpec } from 'start-sdk/types/config-types'
import { Url } from '@start9labs/shared'
import { MarketplaceManifest } from '@start9labs/marketplace'
import { BasicInfo } from 'src/app/pages/developer-routes/developer-menu/form-info'

View File

@@ -1,7 +1,6 @@
import { isEmptyObject } from '@start9labs/shared'
import {
InstalledPackageDataEntry,
MainStatusStarting,
PackageDataEntry,
PackageMainStatus,
PackageState,

View File

@@ -1,4 +1,4 @@
import { ValueSpec, DefaultString } from './config-types'
import { ValueSpec, DefaultString } from 'start-sdk/types/config-types'
export class Range {
min?: number

View File

@@ -1,4 +1,8 @@
import { PackageDataEntry } from '../services/patch-db/data-model'
import {
PackageDataEntry,
PackageMainStatus,
PackageState,
} from '../services/patch-db/data-model'
import {
DependencyStatus,
HealthStatus,
@@ -35,7 +39,7 @@ export function getPackageInfo(entry: PackageDataEntry): PkgInfo {
export interface PkgInfo {
entry: PackageDataEntry
primaryRendering: StatusRendering
primaryStatus: PrimaryStatus
primaryStatus: PrimaryStatus | PackageState | PackageMainStatus
installProgress: ProgressData | null
error: boolean
warning: boolean

4
libs/Cargo.lock generated
View File

@@ -3008,9 +3008,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.91"
version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883"
checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76"
dependencies = [
"indexmap",
"itoa",

View File

@@ -1,6 +1,5 @@
import Deno from "/deno_global.js";
import * as mainModule from "/embassy.js";
function requireParam(param) {
throw new Error(`Missing required parameter ${param}`);
}
@@ -90,7 +89,10 @@ const bindLocal = async (
externalPort = requireParam("externalPort"),
} = requireParam("options"),
) => {
return Deno.core.opAsync("bind_local", internalPort, { name, externalPort });
return Deno.core.opAsync("bind_local", internalPort, {
id: name,
externalPort,
});
};
const bindTor = async (
{
@@ -99,7 +101,10 @@ const bindTor = async (
externalPort = requireParam("externalPort"),
} = requireParam("options"),
) => {
return Deno.core.opAsync("bind_onion", internalPort, { name, externalPort });
return Deno.core.opAsync("bind_onion", internalPort, {
id: name,
externalPort,
});
};
const signalGroup = async (
@@ -180,7 +185,8 @@ const createDir = (
) => Deno.core.opAsync("create_dir", volumeId, path);
const readDir = (
{ volumeId = requireParam("volumeId"), path = requireParam("path") } = requireParam("options"),
{ volumeId = requireParam("volumeId"), path = requireParam("path") } =
requireParam("options"),
) => Deno.core.opAsync("read_dir", volumeId, path);
const removeDir = (
{ volumeId = requireParam("volumeId"), path = requireParam("path") } =
@@ -289,12 +295,15 @@ const effects = {
removeDir,
removeFile,
rename,
restart,
runCommand,
runDaemon,
runRsync,
chmod,
signalGroup,
sleep,
start,
stop,
trace,
warn,
writeFile,

View File

@@ -19,6 +19,7 @@ use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio::io::AsyncReadExt;
use tokio::sync::{mpsc, Mutex};
use tracing::instrument;
pub trait PathForVolumeId: Send + Sync {
fn path_for(
@@ -330,6 +331,7 @@ impl JsExecutionEnvironment {
]
}
#[instrument(skip(self))]
async fn execute(
self,
procedure_name: ProcedureName,
@@ -371,7 +373,6 @@ impl JsExecutionEnvironment {
Ok(())
})
.build();
let loader = std::rc::Rc::new(self.module_loader.clone());
let runtime_options = RuntimeOptions {
module_loader: Some(loader),
@@ -395,7 +396,6 @@ impl JsExecutionEnvironment {
evaluated.await??;
Ok::<_, AnyError>(())
};
let answer = tokio::select! {
Some(x) = receive_answer.recv() => x,
_ = future => {

View File

@@ -24,7 +24,7 @@ rand = "0.8"
regex = "1.7.1"
rpc-toolkit = "0.2.1"
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = "1.0.82"
serde_json = "1.0"
sqlx = { version = "0.6.0", features = [
"chrono",
"offline",

View File

@@ -262,6 +262,15 @@ dependencies = [
"itertools 0.9.0",
]
[[package]]
name = "bimap"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc0455254eb5c6964c4545d8bac815e1a1be4f3afe0ae695ea539c12d728d44b"
dependencies = [
"serde",
]
[[package]]
name = "bincode"
version = "1.3.3"
@@ -1181,6 +1190,7 @@ dependencies = [
"base64 0.13.1",
"base64ct",
"basic-cookies",
"bimap",
"bollard",
"bytes",
"chrono",
@@ -1207,6 +1217,7 @@ dependencies = [
"http",
"hyper",
"hyper-ws-listener",
"id-pool",
"imbl 2.0.0",
"indexmap",
"ipnet",
@@ -1974,6 +1985,15 @@ dependencies = [
"cxx-build",
]
[[package]]
name = "id-pool"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d0df4d8a768821ee4aa2e0353f67125c4586f0e13adbf95b8ebbf8d8fdb344"
dependencies = [
"serde",
]
[[package]]
name = "ident_case"
version = "1.0.1"
@@ -3802,9 +3822,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.91"
version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883"
checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76"
dependencies = [
"indexmap",
"itoa 1.0.5",