feat: better type narrowing for list and array types

This commit is contained in:
Ruben Fiszel
2022-06-22 21:48:41 +02:00
committed by GitHub
parent 6dc90a3906
commit 276319d992
9 changed files with 109 additions and 53 deletions
+8 -1
View File
@@ -3119,13 +3119,20 @@ components:
typ:
oneOf:
- type: string
enum: ["str", "float", "int", "bool", "unknown"]
enum: ["str", "float", "int", "bool", "email", "unknown"]
- type: object
properties:
resource:
type: string
required:
- resource
- type: object
properties:
list:
type: string
enum: ["str", "float", "int", "email"]
required:
- list
has_default:
type: boolean
default: {}
+39 -5
View File
@@ -25,6 +25,16 @@ pub struct MainArgSignature {
pub args: Vec<Arg>,
}
#[derive(Serialize, Clone)]
#[serde(rename_all(serialize = "lowercase"))]
pub enum InnerTyp {
Str,
Int,
Float,
Bytes,
Email,
}
#[derive(Serialize, Clone)]
#[serde(rename_all(serialize = "lowercase"))]
pub enum Typ {
@@ -33,10 +43,11 @@ pub enum Typ {
Float,
Bool,
Dict,
List,
List(InnerTyp),
Bytes,
Datetime,
Resource(String),
Email,
Unknown,
}
@@ -95,7 +106,7 @@ pub fn parse_python_signature(code: &str) -> error::Result<MainArgSignature> {
"int" => Typ::Int,
"bool" => Typ::Bool,
"dict" => Typ::Dict,
"list" => Typ::List,
"list" => Typ::List(InnerTyp::Str),
"bytes" => Typ::Bytes,
"datetime" => Typ::Datetime,
"datetime.datetime" => Typ::Datetime,
@@ -120,7 +131,7 @@ use swc_common::sync::Lrc;
use swc_common::{FileName, SourceMap};
use swc_ecma_ast::{
AssignPat, BindingIdent, Decl, ExportDecl, FnDecl, Ident, ModuleDecl, ModuleItem, Pat,
TsEntityName, TsKeywordTypeKind, TsType, TsTypeRef,
TsArrayType, TsEntityName, TsKeywordTypeKind, TsType, TsTypeRef,
};
use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax, TsConfig};
@@ -239,7 +250,28 @@ fn binding_ident_to_arg(
_ => Typ::Unknown,
},
// TODO: we can do better here and extract the inner type of array
TsType::TsArrayType(_) => Typ::List,
TsType::TsArrayType(TsArrayType { span: _, elem_type }) => {
match &**elem_type {
TsType::TsTypeRef(TsTypeRef {
span: _,
type_name:
TsEntityName::Ident(Ident {
span: _,
sym,
optional: _,
}),
type_params: _,
}) => match sym.to_string().as_str() {
"Base64" => Typ::List(InnerTyp::Bytes),
"Email" => Typ::List(InnerTyp::Email),
"bigint" => Typ::List(InnerTyp::Int),
"number" => Typ::List(InnerTyp::Float),
_ => Typ::List(InnerTyp::Str),
},
//TsType::TsKeywordType(())
_ => Typ::List(InnerTyp::Str),
}
}
TsType::TsTypeRef(TsTypeRef {
span: _,
type_name,
@@ -266,6 +298,8 @@ fn binding_ident_to_arg(
})
.unwrap_or_else(|| "unknown".to_string()),
),
"Base64" => Typ::Bytes,
"Email" => Typ::Email,
_ => Typ::Unknown,
}
}
@@ -755,7 +789,7 @@ def main():
let code = "
export function main(test1: string, test2: string = \"burkina\",
test3: wmill.Resource<'postgres'>, email: email_string) {
test3: wmill.Resource<'postgres'>, b64: Base64, ls: Base64[], email: Email) {
console.log(42)
}
+2 -3
View File
@@ -7,9 +7,8 @@ export {
UserApi, WorkspaceApi
} from './windmill-api/index.ts'
export type string_regex<S extends string> = String
export type string_email = String
export type Email = string
export type Base64 = string
export type Resource<S extends string> = {}
/**
+1 -1
View File
@@ -10,7 +10,7 @@ export interface SchemaProperty {
enum?: string[]
contentEncoding?: 'base64' | 'binary'
format?: string
items?: { type?: 'string' | 'number' }
items?: { type?: 'string' | 'number' | 'bytes', contentEncoding?: 'base64' },
}
export type Schema = {
+11 -6
View File
@@ -44,7 +44,7 @@ export async function inferArgs(
}
}
function argSigToJsonSchemaType(t: string | { resource: string }, s: SchemaProperty): void {
function argSigToJsonSchemaType(t: string | { resource: string } | { list: string }, s: SchemaProperty): void {
if (t === 'int') {
s.type = 'integer'
} else if (t === 'float') {
@@ -55,18 +55,23 @@ function argSigToJsonSchemaType(t: string | { resource: string }, s: SchemaPrope
s.type = 'string'
} else if (t === 'dict') {
s.type = 'object'
} else if (t === 'list') {
s.type = 'array'
} else if (t === 'bytes') {
s.type = 'string'
s.contentEncoding = 'base64'
} else if (t === 'datetime') {
s.type = 'string'
s.format = 'date-time'
} else if (typeof t !== 'string' && t.resource != undefined) {
} else if (typeof t !== 'string' && `resource` in t) {
s.type = 'object'
s.format = `resource-${t.resource}`
} else {
s.type = undefined
} else if (typeof t !== 'string' && `list` in t) {
s.type = 'array'
if (t.list === 'int' || t.list === 'float') {
s.items = { type: 'number' }
} else if (t.list === 'bytes') {
s.items = { type: 'string', contentEncoding: 'base64' }
} else {
s.items = { type: 'string' }
}
}
}
+21 -5
View File
@@ -27,7 +27,8 @@
export let enum_: string[] | undefined = undefined
export let disabled = false
export let editableSchema = false
export let itemsType: { type?: 'string' | 'number' } | undefined = undefined
export let itemsType: { type?: 'string' | 'number'; contentEncoding?: string } | undefined =
undefined
export let displayHeader = true
let seeEditable: boolean = enum_ != undefined || pattern != undefined
@@ -70,16 +71,16 @@
rawValue = JSON.stringify(value, null, 4)
}
function fileChanged(e: any) {
function fileChanged(e: any, cb: (v: string | undefined) => void) {
let t = e.target
if (t && 'files' in t && t.files.length > 0) {
let reader = new FileReader()
reader.onload = (e: any) => {
value = e.target.result.split('base64,')[1]
cb(e.target.result.split('base64,')[1])
}
reader.readAsDataURL(t.files[0])
} else {
value = undefined
cb(undefined)
}
}
@@ -147,6 +148,9 @@
<option value={undefined}>No specific item type</option>
<option value={{ type: 'string' }}> Items are strings</option>
<option value={{ type: 'number' }}>Items are numbers</option>
<option value={{ type: 'string', contentEncoding: 'base64' }}
>Items are bytes</option
>
</select>
{/if}
</label>
@@ -192,6 +196,13 @@
<div class="flex flex-row max-w-md">
{#if itemsType.type == 'number'}
<input type="number" bind:value={v} />
{:else if itemsType.type == 'string' && itemsType.contentEncoding == 'base64'}
<input
type="file"
class="my-6"
on:change={(x) => fileChanged(x, (val) => (v = val))}
multiple={false}
/>
{:else}
<input type="text" bind:value={v} />
{/if}
@@ -236,7 +247,12 @@
{:else if type == 'string' && format == 'date-time'}
<input class="inline-block" type="datetime-local" bind:value />
{:else if type == 'string' && contentEncoding == 'base64'}
<input type="file" class="my-6" on:change={fileChanged} multiple={false} />
<input
type="file"
class="my-6"
on:change={(x) => fileChanged(x, (val) => (value = val))}
multiple={false}
/>
{:else if type == 'string' && format?.startsWith('resource')}
<ResourcePicker
bind:value
@@ -1,7 +1,7 @@
<script lang="ts">
import { faPlus } from '@fortawesome/free-solid-svg-icons'
import type { Schema } from '../../common'
import { emptySchema } from '../../utils'
import { emptySchema, loadSchema } from '../../utils'
import Icon from 'svelte-awesome'
@@ -22,16 +22,13 @@
await Promise.all(
flow.value.modules.map(async (x, i) => {
if (x.value.path) {
const script = await ScriptService.getScriptByPath({
workspace: $workspaceStore!,
path: x.value.path ?? ''
})
const schema = await loadSchema(x.value.path ?? '')
if (
JSON.stringify(Object.keys(script.schema?.properties ?? {}).sort()) !=
JSON.stringify(Object.keys(schema?.properties ?? {}).sort()) !=
JSON.stringify(Object.keys(x.input_transform).sort())
) {
let it = {}
Object.keys(script.schema?.properties ?? {}).map(
Object.keys(schema?.properties ?? {}).map(
(x) =>
(it[x] = {
type: 'static',
@@ -40,7 +37,7 @@
)
schemaForms[i]?.setArgs(it)
}
schemas[i] = script.schema ?? emptySchema()
schemas[i] = schema ?? emptySchema()
} else {
schemaForms[i]?.setArgs({})
schemas[i] = emptySchema()
@@ -84,11 +81,7 @@
disabled={flow.value.modules.length == 0 ||
flow.value.modules[0].value.path == undefined}
on:click={async () => {
const script = await ScriptService.getScriptByPath({
workspace: $workspaceStore ?? '',
path: flow.value.modules[0].value.path ?? ''
})
flow.schema = script.schema
flow.schema = await loadSchema(flow.value.modules[0].value.path ?? '')
}}
>Copy from step 1's schema
</button>
@@ -1,14 +1,11 @@
<script lang="ts">
import { workspaceStore } from '../../stores'
import type { Schema } from '../../common'
import { ScriptService, type Flow, type FlowModule } from '../../gen'
import type { Flow, FlowModule } from '../../gen'
import SchemaForm from './SchemaForm.svelte'
import ScriptPicker from './ScriptPicker.svelte'
import { emptySchema } from '../../utils'
import { emptySchema, loadSchema as UloadSchema } from '../../utils'
import FlowPreview from './FlowPreview.svelte'
import { inferArgs } from '../../infer'
export let flow: Flow
export let i: number
@@ -20,18 +17,7 @@
export async function loadSchema() {
if (mod.value.path) {
let schema
if (mod.value.path.startsWith('hub/')) {
const code = await ScriptService.getHubScriptContentByPath({ path: mod.value.path })
schema = emptySchema()
await inferArgs('deno', code, schema)
} else {
const script = await ScriptService.getScriptByPath({
workspace: $workspaceStore!,
path: mod.value.path ?? ''
})
schema = script.schema
}
let schema = await UloadSchema(mod.value.path)
if (
JSON.stringify(Object.keys(schema?.properties ?? {}).sort()) !=
JSON.stringify(Object.keys(mod.input_transform).sort())
+18 -2
View File
@@ -2,8 +2,9 @@
import { goto } from '$app/navigation'
import { toast } from '@zerodevx/svelte-toast'
import { get } from 'svelte/store'
import { CancelablePromise, UserService, type User } from './gen'
import { clearStores, superadmin, userStore, workspaceStore, type UserExt } from './stores'
import { ScriptService, UserService, type User } from './gen'
import { inferArgs } from './infer'
import { clearStores, superadmin, workspaceStore, type UserExt } from './stores'
export function isToday(someDate: Date): boolean {
const today = new Date()
@@ -138,6 +139,21 @@ export function clickOutside(node: any): any {
}
}
export async function loadSchema(path: string) {
if (path.startsWith('hub/')) {
const code = await ScriptService.getHubScriptContentByPath({ path })
const schema = emptySchema()
await inferArgs('deno', code, schema)
return schema
} else {
const script = await ScriptService.getScriptByPath({
workspace: get(workspaceStore)!,
path: path ?? ''
})
return script.schema
}
}
export type DropdownType = 'action' | 'delete'
export interface DropdownItem {