0.3.0 refactor

ui: adds overlay layer to patch-db-client

ui: getting towards mocks

ui: cleans up factory init

ui: nice type hack

ui: live api for patch

ui: api service source + http

starts up

ui: api source + http

ui: rework patchdb config, pass stashTimeout into patchDbModel

wires in temp patching into api service

ui: example of wiring patchdbmodel into page

begin integration

remove unnecessary method

linting

first data rendering

rework app initialization

http source working for ssh delete call

temp patches working

entire Embassy tab complete

not in kansas anymore

ripping, saving progress

progress for API request response types and endoint defs

Update data-model.ts

shambles, but in a good way

progress

big progress

progress

installed list working

big progress

progress

progress

begin marketplace redesign

Update api-types.ts

Update api-types.ts

marketplace improvements

cosmetic

dependencies and recommendations

begin nym auth approach

install wizard

restore flow and donations
This commit is contained in:
Aaron Greenspan
2021-02-16 13:45:09 -07:00
committed by Aiden McClelland
parent fd685ae32c
commit 594d93eb3b
238 changed files with 15137 additions and 21331 deletions

View File

@@ -0,0 +1,484 @@
import { ValueSpec, ConfigSpec, UniqueBy, ValueSpecOf, ValueType, ValueSpecObject, ValueSpecUnion } from './config-types'
import { Annotations, getDefaultObject, getDefaultUnion, listInnerSpec, mapConfigSpec, Range } from './config-utilities'
import * as pointer from 'json-pointer'
import * as handlebars from 'handlebars'
export class ConfigCursor<T extends ValueType> {
private cachedSpec?: ValueSpecOf<T>
constructor (
private readonly rootSpec: ConfigSpec,
private readonly rootOldConfig: object,
private readonly rootMappedConfig: object = null,
private readonly rootConfig: object = null,
private readonly ptr: string = '',
) {
if (!this.rootOldConfig) {
this.rootOldConfig = getDefaultObject(this.rootSpec)
}
if (!this.rootMappedConfig) {
this.rootMappedConfig = JSON.parse(JSON.stringify(this.rootOldConfig))
mapConfigSpec(this.rootSpec, this.rootMappedConfig)
}
if (!this.rootConfig) {
this.rootConfig = JSON.parse(JSON.stringify(this.rootMappedConfig))
}
}
seek<S extends ValueType> (ptr: string): ConfigCursor<S> {
return new ConfigCursor(
this.rootSpec,
this.rootOldConfig,
this.rootMappedConfig,
this.rootConfig,
pointer.compile(
pointer.parse(this.ptr)
.concat(pointer.parse(ptr)),
),
)
}
seekNext<S extends ValueType> (key: string | number): ConfigCursor<S> {
return this.seek(pointer.compile([`${key}`]))
}
unseek<S extends ValueType> (levels?: number): ConfigCursor<S> {
let ptr: string
if (levels === undefined) {
ptr = ''
} else {
// TODO, delete or make use of, it isn't being used so far
// This is not being used so far
let ptr_arr = pointer.parse(this.ptr)
for (let i = 0; i < levels; i++) {
ptr_arr.pop()
}
ptr = pointer.compile(ptr_arr)
}
return new ConfigCursor(
this.rootSpec,
this.rootOldConfig,
this.rootMappedConfig,
this.rootConfig,
ptr,
)
}
key (): string {
return pointer.parse(this.ptr).pop()
}
oldConfig (): any {
if (pointer.has(this.rootOldConfig, this.ptr)) {
return pointer.get(this.rootOldConfig, this.ptr)
} else {
return undefined
}
}
mappedConfig (): any {
if (pointer.has(this.rootMappedConfig, this.ptr)) {
return pointer.get(this.rootMappedConfig, this.ptr)
} else {
return undefined
}
}
toString (): string {
const spec: ValueSpec = this.spec()
const config = this.config()
switch (spec.type) {
case 'string':
return config
case 'number':
return `${config}${spec.units ? ' ' + spec.units : ''}`
case 'object':
return spec.displayAs ? handlebars.compile(spec.displayAs)(config) : ''
case 'union':
return spec.displayAs ? handlebars.compile(spec.displayAs)(config) : config[spec.tag.id]
case 'pointer':
return 'System Defined'
default:
return ''
}
}
// if (config : T) then (spec : ValueSpecOf<T>)
config (): any {
if (pointer.has(this.rootConfig, this.ptr)) {
return pointer.get(this.rootConfig, this.ptr)
} else {
return undefined
}
}
// if (config : T) then (spec : ValueSpecOf<T>)
spec (): ValueSpecOf<T> {
if (this.cachedSpec) return this.cachedSpec
const parsed = pointer.parse(this.ptr)
// We elevate the rootSpec (ConfigSpec) to a dummy ValueSpecObject
let ret: ValueSpec = {
type: 'object',
spec: this.rootSpec,
nullable: false,
nullByDefault: false,
name: 'Config',
displayAs: 'Config',
uniqueBy: null,
}
let ptr = []
for (let seg of parsed) {
switch (ret.type) {
case 'object':
ret = ret.spec[seg]
break
case 'union':
if (seg === ret.tag.id) {
ret = {
type: 'enum',
default: ret.default,
values: Object.keys(ret.variants),
name: ret.tag.name,
description: ret.tag.description,
valueNames: ret.tag.variantNames,
}
} else {
const cfg = this.unseek().seek(pointer.compile(ptr))
ret = ret.variants[cfg.config()[ret.tag.id]][seg]
}
break
case 'list':
//in essence, for a list we replace the list typed ValueSpecOf with it's internal ListValueSpec, a ValueSpecOf<T> where config @ ptr is of type T[].
// we also append default values to it.
// note also that jsonKey is not used. jsonKey in this case is an index of an array, like 0, 1, etc.
// this implies that every index of a list has an identical inner spec
ret = listInnerSpec(ret)
break
default:
return undefined
}
if (ret === undefined) break
ptr.push(seg)
}
this.cachedSpec = ret as ValueSpecOf<T>
return this.cachedSpec
}
checkInvalid (): string | null { // null if valid
const spec: ValueSpec = this.spec()
const cfg = this.config()
switch (spec.type) {
case 'string':
if (!cfg) {
return spec.nullable ? null : `${spec.name} is missing.`
} else if (typeof cfg === 'string') {
if (!spec.pattern || new RegExp(spec.pattern).test(cfg)) {
return null
} else {
return spec.patternDescription
}
} else {
throw new TypeError(`${this.ptr}: expected string, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`)
}
case 'number':
if (!cfg) {
return spec.nullable ? null : `${spec.name} is missing.`
} else if (typeof cfg === 'number') {
if (spec.integral && cfg != Math.trunc(cfg)) {
return `${spec.name} must be an integer.`
}
try {
Range.from(spec.range).checkIncludes(cfg)
return null
} catch (e) {
return e.message
}
} else {
throw new TypeError(`${this.ptr}: expected number, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`)
}
case 'boolean':
if (typeof cfg === 'boolean') {
return null
} else {
throw new TypeError(`${this.ptr}: expected boolean, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`)
}
case 'enum':
if (typeof cfg === 'string') {
spec.valuesSet = spec.valuesSet || new Set(spec.values)
return spec.valuesSet.has(cfg) ? null : `${cfg} is not a valid selection.`
} else {
throw new TypeError(`${this.ptr}: expected string, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`)
}
case 'list':
if (Array.isArray(cfg)) {
const range = Range.from(spec.range)
const min = range.integralMin()
const max = range.integralMax()
const length = cfg.length
if (min && length < min) {
return spec.subtype === 'enum' ? 'Not enough options selected.' : 'List is too short.'
}
if (max && length > max) {
return spec.subtype === 'enum' ? 'Too many options selected.' : 'List is too long.'
}
for (let idx = 0; idx < cfg.length; idx++) {
let cursor = this.seekNext(idx)
const invalid = cursor.checkInvalid()
if (invalid) {
return `Item #${idx + 1} is invalid. ${invalid}.`
}
if (spec.subtype === 'enum') continue
for (let idx2 = idx + 1; idx2 < cfg.length; idx2++) {
if (cursor.equals(this.seekNext(idx2))) {
return `Item #${idx + 1} is not unique.` + ('uniqueBy' in cursor.spec()) ? `${
displayUniqueBy(
(cursor.spec() as ValueSpecObject | ValueSpecUnion).uniqueBy,
(cursor.spec() as ValueSpecObject | ValueSpecUnion),
cursor.config(),
)
} must be unique.` : ''
}
}
}
return null
} else {
throw new TypeError(`${this.ptr}: expected array, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`)
}
case 'object':
if (!cfg) {
return spec.nullable ? null : `${spec.name} is missing.`
} else if (typeof cfg === 'object' && !Array.isArray(cfg)) {
for (let idx in spec.spec) {
if (this.seekNext(idx).checkInvalid()) {
return `${spec.spec[idx].name} is invalid.`
}
}
return null
} else {
throw new TypeError(`${this.ptr}: expected object, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`)
}
case 'pointer':
return null
case 'union':
if (typeof cfg === 'object' && !Array.isArray(cfg)) {
if (typeof cfg[spec.tag.id] === 'string') {
for (let idx in spec.variants[cfg[spec.tag.id]]) {
if (this.seekNext(idx).checkInvalid()) {
return `${spec.variants[cfg[spec.tag.id]][idx].name} is invalid.`
}
}
return null
} else {
throw new TypeError(`${this.ptr}/${spec.tag.id}: expected string, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`)
}
} else {
throw new TypeError(`${this.ptr}: expected object, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`)
}
}
}
isNew (): boolean {
const oldCfg = this.oldConfig()
const mappedCfg = this.mappedConfig()
if (mappedCfg && oldCfg && typeof mappedCfg === 'object' && typeof oldCfg === 'object') {
for (let key in mappedCfg) {
if (this.seekNext(key).isNew()) return true
}
return false
} else {
return mappedCfg !== oldCfg
}
}
isEdited (): boolean {
const cfg = this.config()
const mappedCfg = this.mappedConfig()
if (cfg && mappedCfg && typeof cfg === 'object' && typeof mappedCfg === 'object') {
const spec = this.spec()
if (spec === undefined) return true
let allKeys: Set<string>
if (spec.type === 'union') {
let unionSpec = spec as ValueSpecOf<'union'>
const labelForSelection = unionSpec.tag.id
allKeys = new Set([labelForSelection, ...Object.keys(unionSpec.variants[cfg[labelForSelection]])])
} else {
allKeys = new Set([...Object.keys(cfg), ...Object.keys(mappedCfg)])
}
for (let key of allKeys) {
if (this.seekNext(key).isEdited()) return true
}
return false
} else {
return cfg !== mappedCfg
}
}
equals (cursor: ConfigCursor<T>): boolean {
const lhs = this.config()
const rhs = cursor.config()
const spec: ValueSpec = this.spec()
switch (spec.type) {
case 'string':
case 'number':
case 'boolean':
case 'enum':
return lhs === rhs
case 'object':
case 'union':
return isEqual(spec.uniqueBy, this as ConfigCursor<'object' | 'union'>, cursor as ConfigCursor<'object' | 'union'>)
case 'list':
if (lhs.length !== rhs.length) {
return false
}
for (let idx = 0; idx < lhs.length; idx++) {
if (!this.seekNext(`${idx}`).equals(cursor.seekNext(`${idx}`))) {
return false
}
}
return true
default:
return false
}
}
getAnnotations (): Annotations<T> {
const spec: ValueSpec = this.spec()
switch (spec.type) {
case 'object': {
const ret: Annotations<'object'> = {
self: {
invalid: this.checkInvalid(),
edited: this.isEdited(),
added: this.isNew(),
},
members: { },
}
for (let key in spec.spec) {
let annotation: any = this.seekNext(key).getAnnotations()
if ('self' in annotation) {
annotation = annotation.self
}
ret.members[key] = annotation
}
return ret as Annotations<T>
}
case 'union': {
const ret: Annotations<'union'> = {
self: {
invalid: this.checkInvalid(),
edited: this.isEdited(),
added: this.isNew(),
},
members: {
[spec.tag.id]: this.seekNext<'enum'>(spec.tag.id).getAnnotations(),
},
}
for (let key in spec.variants[this.config()[spec.tag.id]]) {
let annotation: any = this.seekNext(key).getAnnotations()
if ('self' in annotation) {
annotation = annotation.self
}
ret.members[key] = annotation
}
return ret as Annotations<T>
}
case 'list': {
const ret: Annotations<'list'> = {
self: {
invalid: this.checkInvalid(),
edited: this.isEdited(),
added: this.isNew(),
},
members: [],
}
for (let key in this.config()) {
let annotation: any = this.seekNext(key).getAnnotations()
if ('self' in annotation) {
annotation = annotation.self
}
ret.members[key] = annotation
}
return ret as Annotations<T>
}
default:
return {
invalid: this.checkInvalid(),
edited: this.isEdited(),
added: this.isNew(),
} as Annotations<T>
}
}
async createFirstEntryForList () {
const spec: ValueSpec = this.spec()
if (spec.type === 'object' && !this.config()) {
pointer.set(this.rootConfig, this.ptr, getDefaultObject(spec.spec))
}
if (spec.type === 'union' && !this.config()) {
pointer.set(this.rootConfig, this.ptr, getDefaultUnion(spec))
}
}
injectModalData (res: { data?: any }): void {
if (res.data !== undefined) {
pointer.set(this.rootConfig, this.ptr, res.data)
}
}
}
function isEqual (uniqueBy: UniqueBy, lhs: ConfigCursor<'object'>, rhs: ConfigCursor<'object'>): boolean {
if (uniqueBy === null) {
return false
} else if (typeof uniqueBy === 'string') {
return lhs.seekNext(uniqueBy).equals(rhs.seekNext(uniqueBy))
} else if ('any' in uniqueBy) {
for (let subSpec of uniqueBy.any) {
if (isEqual(subSpec, lhs, rhs)) {
return true
}
}
return false
} else if ('all' in uniqueBy) {
for (let subSpec of uniqueBy.all) {
if (!isEqual(subSpec, lhs, rhs)) {
return false
}
}
return true
}
}
export function displayUniqueBy (uniqueBy: UniqueBy, spec: ValueSpecObject | ValueSpecUnion, value: object): string {
if (typeof uniqueBy === 'string') {
if (spec.type === 'object') {
return spec.spec[uniqueBy].name
} else if (spec.type === 'union') {
if (uniqueBy === spec.tag.id) {
return spec.tag.name
} else {
return spec.variants[value[spec.tag.id]][uniqueBy].name
}
}
} else if ('any' in uniqueBy) {
return uniqueBy.any.map(uq => {
if (typeof uq === 'object' && 'all' in uq) {
return `(${displayUniqueBy(uq, spec, value)})`
} else {
return displayUniqueBy(uq, spec, value)
}
}).join(' and ')
} else if ('all' in uniqueBy) {
return uniqueBy.all.map(uq => {
if (typeof uq === 'object' && 'any' in uq) {
return `(${displayUniqueBy(uq, spec, value)})`
} else {
return displayUniqueBy(uq, spec, value)
}
}).join(' or ')
}
}

View File

@@ -0,0 +1,134 @@
export interface ConfigSpec { [key: string]: ValueSpec }
export type ValueType = 'string' | 'number' | 'boolean' | 'enum' | 'list' | 'object' | 'pointer' | '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 'pointer' ? ValueSpecPointer :
T extends 'union' ? ValueSpecUnion :
never
export interface ValueSpecString extends ListValueSpecString, WithStandalone {
type: 'string'
default?: DefaultString
nullable: boolean
masked: boolean
copyable: 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 extends ListValueSpecUnion, WithStandalone {
type: 'union'
}
export interface ValueSpecPointer extends WithStandalone {
type: 'pointer'
subtype: 'app' | 'system'
target: 'lan-address' | 'tor-address' | 'config'
'app-id': string
}
export interface ValueSpecObject extends ListValueSpecObject, WithStandalone {
type: 'object'
nullable: boolean
nullByDefault: boolean
}
export interface WithStandalone {
name: string
description?: string
changeWarning?: string
}
// no lists of booleans, lists, pointers
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
patternDescription?: string
}
export interface ListValueSpecNumber {
range: string
integral: boolean
units?: string
}
export interface ListValueSpecEnum {
values: string[]
valuesSet?: Set<string>
valueNames: { [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
uniqueBy: UniqueBy //indicates whether duplicates can be permitted in the list
displayAs?: 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 }
displayAs?: 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
uniqueBy: 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
name: string
description?: string
variantNames: { //the name of each variant
[variant: string]: string
}
}
export type DefaultString = string | { charset: string, len: number }

View File

@@ -0,0 +1,413 @@
import {
ValueSpec, ValueSpecList, DefaultString, ValueSpecUnion, ConfigSpec,
ValueSpecObject, ValueSpecString, ValueSpecEnum, ValueSpecNumber,
ValueSpecBoolean, ValueSpecPointer, ValueSpecOf, ListValueSpecType
} from './config-types'
export interface Annotation {
invalid: string | null
edited: boolean
added: boolean
}
export type Annotations<T extends string> =
T extends 'object' | 'union' ? { self: Annotation, members: { [key: string]: Annotation } } :
T extends 'list' ? { self: Annotation, members: Annotation[] } :
Annotation
export class Range {
min?: number
max?: number
minInclusive: boolean
maxInclusive: boolean
static from (s: string): Range {
const r = new Range()
r.minInclusive = s.startsWith('[')
r.maxInclusive = s.endsWith(']')
const [minStr, maxStr] = s.split(',').map(a => a.trim())
r.min = minStr === '(*' ? undefined : Number(minStr.slice(1))
r.max = maxStr === '*)' ? undefined : Number(maxStr.slice(0, -1))
return r
}
checkIncludes (n: number) {
if (this.hasMin() !== undefined && ((!this.minInclusive && this.min == n || (this.min > n)))) {
throw new Error(`Value must be ${this.minMessage()}.`)
}
if (this.hasMax() && ((!this.maxInclusive && this.max == n || (this.max < n)))) {
throw new Error(`Value must be ${this.maxMessage()}.`)
}
}
hasMin (): boolean {
return this.min !== undefined
}
hasMax (): boolean {
return this.max !== undefined
}
minMessage (): string {
return `greater than${this.minInclusive ? ' or equal to' : ''} ${this.min}`
}
maxMessage (): string {
return `less than${this.maxInclusive ? ' or equal to' : ''} ${this.max}`
}
description (): string {
let message = 'Value can be any number.'
if (this.hasMin() || this.hasMax()) {
message = 'Value must be'
}
if (this.hasMin() && this.hasMax()) {
message = `${message} ${this.minMessage()} AND ${this.maxMessage()}.`
} else if (this.hasMin() && !this.hasMax()) {
message = `${message} ${this.minMessage()}.`
} else if (!this.hasMin() && this.hasMax()) {
message = `${message} ${this.maxMessage()}.`
}
return message
}
integralMin (): number | undefined {
if (this.min) {
const ceil = Math.ceil(this.min)
if (this.minInclusive) {
return ceil
} else {
if (ceil === this.min) {
return ceil + 1
} else {
return ceil
}
}
}
}
integralMax (): number | undefined {
if (this.max) {
const floor = Math.floor(this.max)
if (this.maxInclusive) {
return floor
} else {
if (floor === this.max) {
return floor - 1
} else {
return floor
}
}
}
}
}
// converts a ValueSpecList, i.e. a spec for a list, to its inner ListValueSpec, i.e., a spec for the values within the list.
// We then augment it with the defalt values (e.g. nullable: false) to make a
export function listInnerSpec (listSpec: ValueSpecList): ValueSpecOf<ListValueSpecType> {
return {
type: listSpec.subtype,
nullable: false,
name: listSpec.name,
description: listSpec.description,
changeWarning: listSpec.changeWarning,
...listSpec.spec as any, //listSpec.spec is a ListValueSpecOf listSpec.subtype
}
}
export function mapSpecToConfigValue (spec: ValueSpec, value: any): any {
if (value === undefined) return undefined
switch (spec.type) {
case 'string': return mapStringSpec(value)
case 'number': return mapNumberSpec(value)
case 'boolean': return mapBooleanSpec(spec, value)
case 'enum': return mapEnumSpec(spec, value)
case 'list': return mapListSpec(spec, value)
case 'object': return mapObjectSpec(spec, value)
case 'union': return mapUnionSpec(spec, value)
case 'pointer': return value
}
}
export function mapConfigSpec (configSpec: ConfigSpec, value: any): object {
if (value && typeof value === 'object' && !Array.isArray(value)) {
Object.entries(configSpec).map(([key, val]) => {
value[key] = mapSpecToConfigValue(val, value[key])
if (value[key] === undefined) {
value[key] = getDefaultConfigValue(val)
}
})
return value
} else {
return getDefaultObject(configSpec)
}
}
export function mapObjectSpec (spec: ValueSpecObject, value: any): object {
if (value && typeof value === 'object' && !Array.isArray(value)) {
return mapConfigSpec(spec.spec, value)
} else {
return null
}
}
export function mapUnionSpec (spec: ValueSpecUnion, value: any): object {
if (value && typeof value === 'object' && !Array.isArray(value)) {
const variant = mapEnumSpec({
...spec.tag,
type: 'enum',
default: spec.default,
values: Object.keys(spec.variants),
valueNames: spec.tag.variantNames,
}, value[spec.tag.id])
value = mapConfigSpec(spec.variants[variant], value)
value[spec.tag.id] = variant
return value
} else {
return getDefaultUnion(spec)
}
}
export function mapStringSpec (value: any): string {
if (typeof value === 'string') {
return value
} else {
return null
}
}
export function mapNumberSpec (value: any): number {
if (typeof value === 'number') {
return value
} else {
return null
}
}
export function mapEnumSpec (spec: ValueSpecEnum, value: any): string {
if (typeof value === 'string' && spec.values.includes(value)) {
return value
} else {
return spec.default
}
}
export function mapListSpec (spec: ValueSpecList, value: any): string[] | number[] | object[] {
if (Array.isArray(value)) {
const innerSpec = listInnerSpec(spec)
return value.map(item => mapSpecToConfigValue(innerSpec, item))
} else {
return getDefaultList(spec)
}
}
export function mapBooleanSpec (spec: ValueSpecBoolean, value: any): boolean {
if (typeof value === 'boolean') {
return value
} else {
return spec.default
}
}
export function getDefaultConfigValue (spec: ValueSpec): string | number | object | string[] | number[] | object[] | boolean | null {
switch (spec.type) {
case 'object':
return spec.nullByDefault ? null : getDefaultObject(spec.spec)
case 'union':
return getDefaultUnion(spec)
case 'string':
return spec.default ? getDefaultString(spec.default) : null
case 'number':
return spec.default || null
case 'list':
return getDefaultList(spec)
case 'enum':
case 'boolean':
return spec.default
case 'pointer':
return null
}
}
export function getDefaultObject (spec: ConfigSpec): object {
const obj = { }
Object.entries(spec).map(([key, val]) => {
obj[key] = getDefaultConfigValue(val)
})
return obj
}
export function getDefaultList (spec: ValueSpecList): string[] | number[] | object[] {
if (spec.subtype === 'object') {
const l = (spec.default as any[])
const range = Range.from(spec.range)
while (l.length < range.integralMin()) {
l.push(getDefaultConfigValue(listInnerSpec(spec)))
}
return l as string[] | number[] | object[]
} else {
const l = (spec.default as any[]).map(d => getDefaultConfigValue({ ...listInnerSpec(spec), default: d }))
return l as string[] | number[] | object[]
}
}
export function getDefaultUnion (spec: ValueSpecUnion): object {
return { [spec.tag.id]: spec.default, ...getDefaultObject(spec.variants[spec.default]) }
}
export function getDefaultMapTagKey (defaultSpec: DefaultString = '', value: object): string {
const keySrc = getDefaultString(defaultSpec)
const keys = Object.keys(value)
let key = keySrc
let idx = 1
while (keys.includes(key)) {
key = `${keySrc}-${idx++}`
}
return key
}
export function getDefaultString (defaultSpec: DefaultString): string {
if (typeof defaultSpec === 'string') {
return defaultSpec
} else {
let s = ''
for (let i = 0; i < defaultSpec.len; i++) {
s = s + getRandomCharInSet(defaultSpec.charset)
}
return s
}
}
export function getDefaultDescription (spec: ValueSpec): string {
let toReturn: string | undefined
switch (spec.type) {
case 'string':
if (typeof spec.default === 'string') {
toReturn = spec.default
} else if (typeof spec.default === 'object') {
toReturn = 'random'
}
break
case 'number':
if (typeof spec.default === 'number') {
toReturn = String(spec.default)
}
break
case 'boolean':
toReturn = spec.default === true ? 'True' : 'False'
break
case 'enum':
toReturn = spec.valueNames[spec.default]
break
}
return toReturn || ''
}
// a,g,h,A-Z,,,,-
export function getRandomCharInSet (charset: string): string {
const set = stringToCharSet(charset)
let charIdx = Math.floor(Math.random() * set.len)
for (let range of set.ranges) {
if (range.len > charIdx) {
return String.fromCharCode(range.start.charCodeAt(0) + charIdx)
}
charIdx -= range.len
}
throw new Error('unreachable')
}
function stringToCharSet (charset: string): CharSet {
let set: CharSet = { ranges: [], len: 0 }
let start: string | null = null
let end: string | null = null
let in_range = false
for (let char of charset) {
switch (char) {
case ',':
if (start !== null && end !== null) {
if (start!.charCodeAt(0) > end!.charCodeAt(0)) {
throw new Error('start > end of charset')
}
const len = end.charCodeAt(0) - start.charCodeAt(0) + 1
set.ranges.push({
start,
end,
len,
})
set.len += len
start = null
end = null
in_range = false
} else if (start !== null && !in_range) {
set.len += 1
set.ranges.push({ start, end: start, len: 1 })
start = null
} else if (start !== null && in_range) {
end = ','
} else if (start === null && end === null && !in_range) {
start = ','
} else {
throw new Error('unexpected ","')
}
break
case '-':
if (start === null) {
start = '-'
} else if (!in_range) {
in_range = true
} else if (in_range && end === null) {
end = '-'
} else {
throw new Error('unexpected "-"')
}
break
default:
if (start === null) {
start = char
} else if (in_range && end === null) {
end = char
} else {
throw new Error(`unexpected "${char}"`)
}
}
}
if (start !== null && end !== null) {
if (start!.charCodeAt(0) > end!.charCodeAt(0)) {
throw new Error('start > end of charset')
}
const len = end.charCodeAt(0) - start.charCodeAt(0) + 1
set.ranges.push({
start,
end,
len,
})
set.len += len
} else if (start !== null) {
set.len += 1
set.ranges.push({
start,
end: start,
len: 1,
})
}
return set
}
interface CharSet {
ranges: {
start: string
end: string
len: number
}[]
len: number
}

View File

@@ -0,0 +1,27 @@
import { ConfigCursor } from './config-cursor'
import { TrackingModalController } from '../services/tracking-modal-controller.service'
export class ModalPresentable {
constructor (private readonly trackingModalCtrl: TrackingModalController) { }
async presentModal (cursor: ConfigCursor<any>, callback: () => any) {
const modal = await this.trackingModalCtrl.createConfigModal({
backdropDismiss: false,
presentingElement: await this.trackingModalCtrl.getTop(),
componentProps: {
cursor,
},
}, cursor.spec().type)
modal.onWillDismiss().then(res => {
cursor.injectModalData(res)
callback()
})
await modal.present()
}
dismissModal (a: any) {
return this.trackingModalCtrl.dismiss(a)
}
}