From 30fc24af461ea490f6db56dc54e323eec5d7f922 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl?= Date: Wed, 6 Sep 2023 13:08:13 +0300 Subject: [PATCH] feat(sdk): implement injection (#403) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michaël --- libs/common/src/typegraph/types.rs | 2 +- typegate/tests/injection/injection.ts | 55 ++++++++ typegate/tests/injection/injection_test.ts | 51 ++++++++ typegraph/core/src/global_store.rs | 6 + typegraph/core/src/lib.rs | 8 +- typegraph/core/src/typedef/mod.rs | 3 +- typegraph/core/src/typedef/with_injection.rs | 101 +++++++++++++++ .../src/typedef/{policy.rs => with_policy.rs} | 0 typegraph/core/src/typegraph.rs | 6 + typegraph/core/src/types.rs | 6 +- typegraph/core/wit/typegraph.wit | 8 +- typegraph/deno/src/effects.ts | 11 ++ typegraph/deno/src/types.ts | 92 +++++++++++++- typegraph/deno/src/utils/func_utils.ts | 53 ++++++++ typegraph/deno/src/utils/type_utils.ts | 11 ++ .../python_next/typegraph_next/effects.py | 15 +++ typegraph/python_next/typegraph_next/t.py | 120 ++++++++++++++++-- typegraph/python_next/typegraph_next/utils.py | 21 ++- 18 files changed, 548 insertions(+), 21 deletions(-) create mode 100644 typegate/tests/injection/injection.ts create mode 100644 typegraph/core/src/typedef/with_injection.rs rename typegraph/core/src/typedef/{policy.rs => with_policy.rs} (100%) diff --git a/libs/common/src/typegraph/types.rs b/libs/common/src/typegraph/types.rs index b07b2a6f83..7407376193 100644 --- a/libs/common/src/typegraph/types.rs +++ b/libs/common/src/typegraph/types.rs @@ -14,7 +14,7 @@ use super::{EffectType, PolicyIndices}; #[cfg_attr(feature = "codegen", derive(JsonSchema))] #[derive(Serialize, Deserialize, Clone, Debug)] pub struct SingleValue { - value: T, + pub value: T, } #[cfg_attr(feature = "codegen", derive(JsonSchema))] diff --git a/typegate/tests/injection/injection.ts b/typegate/tests/injection/injection.ts new file mode 100644 index 0000000000..cbb6c08a94 --- /dev/null +++ b/typegate/tests/injection/injection.ts @@ -0,0 +1,55 @@ +// Copyright Metatype OÜ, licensed under the Elastic License 2.0. +// SPDX-License-Identifier: Elastic-2.0 + +import { Policy, t, typegraph } from "@typegraph/deno/src/mod.ts"; +import { CREATE, DELETE, NONE, UPDATE } from "@typegraph/deno/src/effects.ts"; +import { DenoRuntime } from "@typegraph/deno/src/runtimes/deno.ts"; + +const tpe = t.struct({ + "a": t.integer({}, { name: "A" }), + "raw_int": t.integer().set({ + [CREATE]: 1, + [UPDATE]: 2, + [DELETE]: 3, + [NONE]: 4, + }), + "raw_str": t.string().set("2"), + "secret": t.integer().fromSecret("TEST_VAR"), + "context": t.string().fromContext("userId"), + "optional_context": t.string().optional().fromContext("inexistent"), + "raw_obj": t.struct({ "in": t.integer() }).set({ "in": -1 }), + "alt_raw": t.string().set("2"), + "alt_secret": t.string().fromSecret("TEST_VAR"), + "alt_context": t.string().fromContext("userId"), + "alt_context_opt": t.string().optional().fromContext("userId"), + "alt_context_opt_missing": t.string().optional().fromContext("userId"), + "date": t.datetime().inject("now"), +}); + +typegraph("injection", (g) => { + const deno = new DenoRuntime(); + const pub = Policy.public(); + + g.expose({ + test: deno.func( + t.struct({ input: tpe }), + t.struct({ + fromInput: tpe, + parent: t.integer({}, { name: "someName" }), + fromParent: deno.func( + t.struct({ + value: t.integer().fromParent({ + [NONE]: "someName", + }), + }), + t.struct({ value: t.integer() }), + { code: "(value) => value" }, + ), + }), + { + code: + "({ input }) => { return { fromInput: input, parent: 1234567 }; }", + }, + ).withPolicy(pub), + }); +}); diff --git a/typegate/tests/injection/injection_test.ts b/typegate/tests/injection/injection_test.ts index f60978a1c2..5c935a3121 100644 --- a/typegate/tests/injection/injection_test.ts +++ b/typegate/tests/injection/injection_test.ts @@ -252,3 +252,54 @@ Meta.test("dynamic value injection", async (t) => { }); unfreezeDate(); }); + +Meta.test("Deno: value injection", async (t) => { + const e = await t.engine("injection/injection.ts", { + secrets: { TG_INJECTION_TEST_VAR: "3" }, + }); + + freezeDate(); + await t.should("work", async () => { + await gql` + query { + test(input: {a: 12}) { + fromInput { + a + context + optional_context + raw_int + raw_obj { in } + alt_raw + alt_secret + alt_context_opt + alt_context_opt_missing + date + } + parent + fromParent { value } + } + }` + .withContext({ + userId: "123", + }) + .expectData({ + test: { + fromInput: { + a: 12, + context: "123", + raw_int: 4, + raw_obj: { in: -1 }, + alt_raw: "2", + alt_secret: "3", + alt_context_opt: "123", + alt_context_opt_missing: "123", + date: new Date().toISOString(), + }, + parent: 1234567, + fromParent: { value: 1234567 }, + }, + }) + .on(e); + }); + unfreezeDate(); +}); diff --git a/typegraph/core/src/global_store.rs b/typegraph/core/src/global_store.rs index 9d3150b8ab..ee8a9f99b7 100644 --- a/typegraph/core/src/global_store.rs +++ b/typegraph/core/src/global_store.rs @@ -70,6 +70,12 @@ impl Store { .ok_or_else(|| errors::object_not_found("type", type_id)) } + pub fn get_type_mut(&mut self, type_id: TypeId) -> Result<&mut Type, TgError> { + self.types + .get_mut(type_id as usize) + .ok_or_else(|| errors::object_not_found("type", type_id)) + } + pub fn get_type_by_name(&self, name: &str) -> Option { self.type_by_names.get(name).copied() } diff --git a/typegraph/core/src/lib.rs b/typegraph/core/src/lib.rs index 339e7c7058..0b32ad9973 100644 --- a/typegraph/core/src/lib.rs +++ b/typegraph/core/src/lib.rs @@ -18,13 +18,13 @@ use indoc::formatdoc; use regex::Regex; use types::{ Array, Boolean, Either, Float, Func, Integer, Optional, Proxy, StringT, Struct, Type, - TypeBoolean, Union, WithPolicy, + TypeBoolean, Union, WithInjection, WithPolicy, }; use validation::validate_name; use wit::core::{ ContextCheck, Policy, PolicyId, TypeArray, TypeBase, TypeEither, TypeFloat, TypeFunc, TypeId, TypeInteger, TypeOptional, TypePolicy, TypeProxy, TypeString, TypeStruct, TypeUnion, - TypegraphInitParams, + TypeWithInjection, TypegraphInitParams, }; use wit::runtimes::{MaterializerDenoFunc, Runtimes}; @@ -198,6 +198,10 @@ impl wit::core::Core for Lib { }) } + fn with_injection(data: TypeWithInjection) -> Result { + with_store_mut(|s| Ok(s.add_type(|id| Type::WithInjection(WithInjection { id, data })))) + } + fn with_policy(data: TypePolicy) -> Result { with_store_mut(|s| Ok(s.add_type(|id| Type::WithPolicy(WithPolicy { id, data })))) } diff --git a/typegraph/core/src/typedef/mod.rs b/typegraph/core/src/typedef/mod.rs index 5a4fde4c39..3890ad94d5 100644 --- a/typegraph/core/src/typedef/mod.rs +++ b/typegraph/core/src/typedef/mod.rs @@ -8,8 +8,9 @@ pub mod float; pub mod func; pub mod integer; pub mod optional; -pub mod policy; pub mod proxy; pub mod string; pub mod struct_; pub mod union; +pub mod with_injection; +pub mod with_policy; diff --git a/typegraph/core/src/typedef/with_injection.rs b/typegraph/core/src/typedef/with_injection.rs new file mode 100644 index 0000000000..ec8066ce4a --- /dev/null +++ b/typegraph/core/src/typedef/with_injection.rs @@ -0,0 +1,101 @@ +// Copyright Metatype OÜ, licensed under the Mozilla Public License Version 2.0. +// SPDX-License-Identifier: MPL-2.0 + +use crate::{ + conversion::types::TypeConversion, + errors::Result, + global_store::{with_store, Store}, + typegraph::TypegraphContext, + types::{Type, TypeData, WithInjection, WrapperTypeData}, + wit::core::TypeWithInjection, +}; +use common::typegraph::{EffectType, Injection, InjectionData, SingleValue, TypeNode}; + +use std::collections::HashMap; + +impl TypeConversion for WithInjection { + fn convert(&self, ctx: &mut TypegraphContext, runtime_id: Option) -> Result { + with_store(|s| -> Result<_> { + let tpe = s.get_type(self.data.tpe)?; + let mut type_node = tpe.convert(ctx, runtime_id)?; + let base = type_node.base_mut(); + let value: Injection = + serde_json::from_str(&self.data.injection).map_err(|e| e.to_string())?; + if let Injection::Parent(data) = value { + let get_correct_id = |v: u32| -> Result { + let id = s.resolve_proxy(v)?; + if let Some(index) = ctx.find_type_index_by_store_id(&id) { + return Ok(index); + } + Err(format!("unable to find type for store id {}", id)) + }; + let new_data = match data { + InjectionData::SingleValue(SingleValue { value }) => { + InjectionData::SingleValue(SingleValue { + value: get_correct_id(value)?, + }) + } + InjectionData::ValueByEffect(per_effect) => { + let mut new_per_effect: HashMap = HashMap::new(); + for (k, v) in per_effect.iter() { + new_per_effect.insert(*k, get_correct_id(*v)?); + } + InjectionData::ValueByEffect(new_per_effect) + } + }; + base.injection = Some(Injection::Parent(new_data)); + } else { + base.injection = Some(value); + } + Ok(type_node) + }) + } +} + +impl TypeData for TypeWithInjection { + fn get_display_params_into(&self, params: &mut Vec) { + let value: Injection = serde_json::from_str(&self.injection).unwrap(); + let gen_display = |data: InjectionData| match data { + InjectionData::SingleValue(t) => t.value, + InjectionData::ValueByEffect(t) => { + let mut res: Vec = vec![]; + for (effect, value) in t.iter() { + res.push(format!("{:?}:{}", effect, value)); + } + res.join(", ") + } + }; + + let gen_display_u32 = |data: InjectionData| match data { + InjectionData::SingleValue(t) => t.value.to_string(), + InjectionData::ValueByEffect(t) => { + let mut res: Vec = vec![]; + for (effect, value) in t.iter() { + res.push(format!("{:?}:{}", effect, value)); + } + res.join(", ") + } + }; + + params.push(format!( + "injection='[{}]'", + match value { + Injection::Static(data) => format!("static={}", gen_display(data)), + Injection::Context(data) => format!("context={}", gen_display(data)), + Injection::Secret(data) => format!("secret={}", gen_display(data)), + Injection::Dynamic(data) => format!("dynamic={}", gen_display(data)), + Injection::Parent(data) => format!("parent={}", gen_display_u32(data)), + }, + )); + } + + fn variant_name(&self) -> String { + "injection".to_string() + } +} + +impl WrapperTypeData for TypeWithInjection { + fn get_wrapped_type<'a>(&self, store: &'a Store) -> Option<&'a Type> { + store.get_type(self.tpe).ok() + } +} diff --git a/typegraph/core/src/typedef/policy.rs b/typegraph/core/src/typedef/with_policy.rs similarity index 100% rename from typegraph/core/src/typedef/policy.rs rename to typegraph/core/src/typedef/with_policy.rs diff --git a/typegraph/core/src/typegraph.rs b/typegraph/core/src/typegraph.rs index c1c357d1f2..b0946d4b6f 100644 --- a/typegraph/core/src/typegraph.rs +++ b/typegraph/core/src/typegraph.rs @@ -203,6 +203,7 @@ impl TypegraphContext { let tpe = s.get_type(type_id)?; let tpe = match tpe { Type::WithPolicy(t) => t.data.get_wrapped_type(s).unwrap(), + Type::WithInjection(t) => t.data.get_wrapped_type(s).unwrap(), _ => tpe, }; if !matches!(tpe, Type::Func(_)) { @@ -238,6 +239,7 @@ impl TypegraphContext { self.types.push(None); let tpe = store.get_type(id)?; + let type_node = tpe.convert(self, runtime_id)?; self.types[idx] = Some(type_node); @@ -328,4 +330,8 @@ impl TypegraphContext { Ok(idx as RuntimeId) } } + + pub fn find_type_index_by_store_id(&self, id: &u32) -> Option { + self.mapping.types.get(id).copied() + } } diff --git a/typegraph/core/src/types.rs b/typegraph/core/src/types.rs index 86571fa297..16e6773431 100644 --- a/typegraph/core/src/types.rs +++ b/typegraph/core/src/types.rs @@ -10,7 +10,7 @@ use crate::global_store::{with_store, Store}; use crate::typegraph::TypegraphContext; use crate::wit::core::{ TypeArray, TypeBase, TypeEither, TypeFloat, TypeFunc, TypeId, TypeInteger, TypeOptional, - TypePolicy, TypeProxy, TypeString, TypeStruct, TypeUnion, + TypePolicy, TypeProxy, TypeString, TypeStruct, TypeUnion, TypeWithInjection, }; pub trait TypeData { @@ -59,7 +59,10 @@ pub type Array = ConcreteType; pub type Optional = ConcreteType; pub type Union = ConcreteType; pub type Either = ConcreteType; + +// Note: TypePolicy|TypeWithInjection|Proxy => Struct | Integer | ... pub type WithPolicy = WrapperType; +pub type WithInjection = WrapperType; #[derive(Debug)] #[enum_dispatch(TypeFun, TypeConversion)] @@ -76,6 +79,7 @@ pub enum Type { Union(Union), Either(Either), WithPolicy(WithPolicy), + WithInjection(WithInjection), } #[enum_dispatch] diff --git a/typegraph/core/wit/typegraph.wit b/typegraph/core/wit/typegraph.wit index fedf6f91dd..f0b4ba7c47 100644 --- a/typegraph/core/wit/typegraph.wit +++ b/typegraph/core/wit/typegraph.wit @@ -15,13 +15,19 @@ interface core { finalize-typegraph: func() -> result type type-id = u32 - record type-base { name: option, // string => json string runtime-config: option>> } + record type-with-injection { + tpe: type-id, + injection: string + } + + with-injection: func(data: type-with-injection) -> result + record type-proxy { name: string, } diff --git a/typegraph/deno/src/effects.ts b/typegraph/deno/src/effects.ts index 5f4e247480..3a81720a79 100644 --- a/typegraph/deno/src/effects.ts +++ b/typegraph/deno/src/effects.ts @@ -23,3 +23,14 @@ export function delete_(idempotent = true): EffectDelete { export function update(idempotent = true): EffectUpdate { return { tag: "update", val: idempotent }; } + +export const UPDATE = Symbol("update"); +export const DELETE = Symbol("delete"); +export const CREATE = Symbol("create"); +export const NONE = Symbol("none"); +export type PerEffect = { + [CREATE]?: string; + [UPDATE]?: string; + [DELETE]?: string; + [NONE]?: string; +}; diff --git a/typegraph/deno/src/types.ts b/typegraph/deno/src/types.ts index db1deeab03..501ba672a1 100644 --- a/typegraph/deno/src/types.ts +++ b/typegraph/deno/src/types.ts @@ -16,7 +16,12 @@ import { import { Materializer } from "./runtimes/mod.ts"; import { mapValues } from "./deps.ts"; import Policy from "./policy.ts"; -import { serializeRecordValues } from "./utils/func_utils.ts"; +import { + serializeInjection, + serializeRecordValues, +} from "./utils/func_utils.ts"; +import { CREATE, DELETE, NONE, UPDATE } from "./effects.ts"; +import { InjectionValue } from "./utils/type_utils.ts"; export type PolicySpec = Policy | { none: Policy; @@ -85,6 +90,87 @@ export class Typedef { } return optional(this, data); } + + private withInjection(injection: string) { + const wrapperId = core.withInjection({ + tpe: this._id, + injection, + }); + return new Proxy(this, { + get(target, prop, receiver) { + return prop === "_id" ? wrapperId : Reflect.get(target, prop, receiver); + }, + }) as this; + } + + set(value: InjectionValue) { + return this.withInjection( + serializeInjection("static", value, (x: unknown) => JSON.stringify(x)), + ); + } + + inject(value: InjectionValue) { + return this.withInjection( + serializeInjection("dynamic", value), + ); + } + + fromContext(value: InjectionValue) { + return this.withInjection( + serializeInjection("context", value), + ); + } + + fromSecret(value: InjectionValue) { + return this.withInjection( + serializeInjection("secret", value), + ); + } + + fromParent(value: InjectionValue) { + let correctValue: any = null; + if (typeof value === "string") { + correctValue = proxy(value)._id; + } else { + const isObject = typeof value === "object" && !Array.isArray(value) && + value !== null; + if (!isObject) { + throw new Error("type not supported"); + } + + // Note: + // Symbol changes the behavior of keys, values, entries => props are skipped + const symbols = [UPDATE, DELETE, CREATE, NONE]; + const noOtherType = Object.keys(value).length == 0; + const isPerEffect = noOtherType && + symbols + .some((symbol) => (value as any)?.[symbol] !== undefined); + + if (!isPerEffect) { + throw new Error("object keys should be of type EffectType"); + } + + correctValue = {}; + for (const symbol of symbols) { + const v = (value as any)?.[symbol]; + if (v === undefined) continue; + if (typeof v !== "string") { + throw new Error( + `value for field ${symbol.toString()} must be a string`, + ); + } + correctValue[symbol] = proxy(v)._id; + } + } + + return this.withInjection( + serializeInjection( + "parent", + correctValue, + (x: unknown) => x as number, + ), + ); + } } class TypeProxy extends Typedef { @@ -243,6 +329,10 @@ export function path() { return string({ format: "path" }); } +export function datetime() { + return string({ format: "date-time" }); +} + // Note: enum is a reserved word export function enum_(variants: string[], base: SimplifiedBase = {}) { return string({ diff --git a/typegraph/deno/src/utils/func_utils.ts b/typegraph/deno/src/utils/func_utils.ts index de1a0e54a0..76d0232739 100644 --- a/typegraph/deno/src/utils/func_utils.ts +++ b/typegraph/deno/src/utils/func_utils.ts @@ -1,6 +1,59 @@ // Copyright Metatype OÜ, licensed under the Mozilla Public License Version 2.0. // SPDX-License-Identifier: MPL-2.0 +import { CREATE, DELETE, NONE, UPDATE } from "../effects.ts"; +import { InjectionSource, InjectionValue } from "./type_utils.ts"; + +export function stringifySymbol(symbol: symbol) { + const name = symbol.toString().match(/\((.+)\)/)?.[1]; + if (!name) { + throw new Error("unable to determine symbol name"); + } + return name; +} + +export function serializeInjection( + source: InjectionSource, + value: InjectionValue, + valueMapper = (value: InjectionValue) => value, +) { + if ( + typeof value === "object" && + !Array.isArray(value) && + value !== null + ) { + // Note: + // Symbol changes the behavior of keys, values, entries => props are skipped + const symbols = [UPDATE, DELETE, CREATE, NONE]; + const noOtherType = Object.keys(value).length == 0; + const isPerEffect = noOtherType && + symbols + .some((symbol) => (value as any)?.[symbol] !== undefined); + + if (isPerEffect) { + const dataEntries = symbols.map( + (symbol) => { + const valueGiven = (value as any)?.[symbol]; + return [ + stringifySymbol(symbol), + valueGiven && valueMapper(valueGiven), + ]; + }, + ); + + return JSON.stringify({ + source, + data: Object.fromEntries(dataEntries), + }); + } + } + + return JSON.stringify({ + source, + data: { value: valueMapper(value) }, + }); +} + export function serializeRecordValues( obj: Record, ): Array<[string, string]> { diff --git a/typegraph/deno/src/utils/type_utils.ts b/typegraph/deno/src/utils/type_utils.ts index e2e3b08439..fc35f3ea80 100644 --- a/typegraph/deno/src/utils/type_utils.ts +++ b/typegraph/deno/src/utils/type_utils.ts @@ -1,6 +1,8 @@ // Copyright Metatype OÜ, licensed under the Mozilla Public License Version 2.0. // SPDX-License-Identifier: MPL-2.0 +import { PerEffect } from "../effects.ts"; + type RequiredKeys = { [K in keyof T]-?: {} extends { [P in K]: T[K] } ? never : K; }[keyof T]; @@ -15,3 +17,12 @@ type PickOptional = Pick>; type Nullable = { [P in keyof T]: T[P] | null }; export type NullableOptional = PickRequired & Nullable>; + +export type InjectionSource = + | "dynamic" + | "static" + | "context" + | "parent" + | "secret"; + +export type InjectionValue = T | PerEffect; diff --git a/typegraph/python_next/typegraph_next/effects.py b/typegraph/python_next/typegraph_next/effects.py index 152ddeae71..72181e72c5 100644 --- a/typegraph/python_next/typegraph_next/effects.py +++ b/typegraph/python_next/typegraph_next/effects.py @@ -7,6 +7,7 @@ EffectNone, EffectUpdate, ) +from enum import auto, Enum def none(): @@ -23,3 +24,17 @@ def update(idempotent: bool = False): def delete(idempotent: bool = True): return EffectDelete(idempotent) + + +# For injections +class EffectType(Enum): + CREATE = auto() + UPDATE = auto() + DELETE = auto() + NONE = auto() + + +CREATE = EffectType.CREATE +UPDATE = EffectType.UPDATE +DELETE = EffectType.DELETE +NONE = EffectType.NONE diff --git a/typegraph/python_next/typegraph_next/t.py b/typegraph/python_next/typegraph_next/t.py index 9f94363de4..78469e778b 100644 --- a/typegraph/python_next/typegraph_next/t.py +++ b/typegraph/python_next/typegraph_next/t.py @@ -3,12 +3,13 @@ import json from typing import Dict, List, Optional, Tuple, Union -from typegraph_next.utils import serialize_record_values +from typegraph_next.utils import serialize_record_values, serialize_injection from typing_extensions import Self from typegraph_next.gen.exports.core import ( PolicyPerEffect as WitPolicyPerEffect, + TypeWithInjection, ) from typegraph_next.gen.exports.core import ( PolicySpecPerEffect, @@ -30,6 +31,7 @@ from typegraph_next.graph.typegraph import core, store from typegraph_next.policy import Policy, PolicyPerEffect from typegraph_next.runtimes.deno import Materializer +from typegraph_next.effects import EffectType class typedef: @@ -72,11 +74,64 @@ def with_policy(self, *policies: Union[Policy, PolicyPerEffect]) -> Self: return _TypeWithPolicy(res.value, self, policies) + def _with_injection(self, injection: str) -> Self: + res = core.with_injection( + store, TypeWithInjection(tpe=self.id, injection=injection) + ) + if isinstance(res, Err): + raise Exception(res.value) + + return _TypeWrapper(res.value, self) + def optional(self, default_value: Optional[str] = None) -> "optional": if isinstance(self, optional): return self return optional(self, default_item=default_value) + def set(self, value: Union[any, Dict[EffectType, any]]): + return self._with_injection( + serialize_injection( + "static", value=value, value_mapper=lambda x: json.dumps(x) + ), + ) + + def inject(self, value: Union[any, Dict[EffectType, any]]): + return self._with_injection(serialize_injection("dynamic", value=value)) + + def from_context(self, value: Union[str, Dict[EffectType, str]]): + return self._with_injection(serialize_injection("context", value=value)) + + def from_secret(self, value: Union[str, Dict[EffectType, str]]): + return self._with_injection(serialize_injection("secret", value=value)) + + def from_parent(self, value: Union[str, Dict[EffectType, str]]): + correct_value = None + if isinstance(value, str): + correct_value = proxy(value).id + else: + if not isinstance(value, dict): + raise Exception("type not supported") + + is_per_effect = len(value) > 0 and all( + isinstance(k, EffectType) for k in value.keys() + ) + if not is_per_effect: + raise Exception("object keys should be of type EffectType") + + correct_value = {} + for k, v in value.items(): + if not isinstance(v, str): + raise Exception(f"value for field {k.name} must be a string") + correct_value[k] = proxy(v).id + + assert correct_value is not None + + return self._with_injection( + serialize_injection( + "parent", value=correct_value, value_mapper=lambda x: x + ), + ) + class _TypeWithPolicy(typedef): base: "typedef" @@ -98,6 +153,25 @@ def __getattr__(self, name): return getattr(self.base, name) +# self.id refer to the wrapper id +# self.* refer to the base id +class _TypeWrapper(typedef): + base: "typedef" + + def __init__( + self, + id: int, + base: "typedef", + ): + super().__init__(id) + self.base = base + + def __getattr__(self, name): + if name == "id": + return self.id + return getattr(self.base, name) + + class ref(typedef): name: str @@ -131,7 +205,7 @@ def __init__( multiple_of: Optional[int] = None, enumeration: Optional[List[int]] = None, name: Optional[str] = None, - config: Optional[Dict[str, any]] = None + config: Optional[Dict[str, any]] = None, ): data = TypeInteger( min=min, @@ -143,7 +217,9 @@ def __init__( ) runtime_config = serialize_record_values(config) res = core.integerb( - store, data, TypeBase(name=name, runtime_config=runtime_config) + store, + data, + TypeBase(name=name, runtime_config=runtime_config), ) if isinstance(res, Err): raise Exception(res.value) @@ -175,7 +251,7 @@ def __init__( multiple_of: Optional[float] = None, enumeration: Optional[List[float]] = None, name: Optional[str] = None, - config: Optional[Dict[str, any]] = None + config: Optional[Dict[str, any]] = None, ): data = TypeFloat( min=min, @@ -187,7 +263,9 @@ def __init__( ) runtime_config = serialize_record_values(config) res = core.floatb( - store, data, TypeBase(name=name, runtime_config=runtime_config) + store, + data, + TypeBase(name=name, runtime_config=runtime_config), ) if isinstance(res, Err): raise Exception(res.value) @@ -229,7 +307,7 @@ def __init__( format: Optional[str] = None, enumeration: Optional[List[str]] = None, name: Optional[str] = None, - config: Optional[Dict[str, any]] = None + config: Optional[Dict[str, any]] = None, ): enum_variants = None if enumeration is not None: @@ -241,7 +319,9 @@ def __init__( runtime_config = serialize_record_values(config) res = core.stringb( - store, data, TypeBase(name=name, runtime_config=runtime_config) + store, + data, + TypeBase(name=name, runtime_config=runtime_config), ) if isinstance(res, Err): raise Exception(res.value) @@ -274,6 +354,10 @@ def path() -> string: return string(format="path") +def datetime() -> string: + return string(format="date-time") + + def enum( variants: List[str], name: Optional[str] = None, @@ -305,7 +389,9 @@ def __init__( runtime_config = serialize_record_values(config) res = core.arrayb( - store, data, TypeBase(name=name, runtime_config=runtime_config) + store, + data, + TypeBase(name=name, runtime_config=runtime_config), ) if isinstance(res, Err): raise Exception(res.value) @@ -335,7 +421,9 @@ def __init__( runtime_config = serialize_record_values(config) res = core.optionalb( - store, data, TypeBase(name=name, runtime_config=runtime_config) + store, + data, + TypeBase(name=name, runtime_config=runtime_config), ) if isinstance(res, Err): raise Exception(res.value) @@ -358,7 +446,9 @@ def __init__( runtime_config = serialize_record_values(config) res = core.unionb( - store, data, TypeBase(name=name, runtime_config=runtime_config) + store, + data, + TypeBase(name=name, runtime_config=runtime_config), ) if isinstance(res, Err): raise Exception(res.value) @@ -380,7 +470,9 @@ def __init__( runtime_config = serialize_record_values(config) res = core.eitherb( - store, data, TypeBase(name=name, runtime_config=runtime_config) + store, + data, + TypeBase(name=name, runtime_config=runtime_config), ) if isinstance(res, Err): raise Exception(res.value) @@ -397,13 +489,15 @@ def __init__( props: Dict[str, typedef], *, name: Optional[str] = None, - config: Optional[Dict[str, str]] = None + config: Optional[Dict[str, str]] = None, ): data = TypeStruct(props=list((name, tpe.id) for (name, tpe) in props.items())) runtime_config = serialize_record_values(config) res = core.structb( - store, data, base=TypeBase(name=name, runtime_config=runtime_config) + store, + data, + base=TypeBase(name=name, runtime_config=runtime_config), ) if isinstance(res, Err): raise Exception(res.value) diff --git a/typegraph/python_next/typegraph_next/utils.py b/typegraph/python_next/typegraph_next/utils.py index 50c48d5298..8c4f4476ac 100644 --- a/typegraph/python_next/typegraph_next/utils.py +++ b/typegraph/python_next/typegraph_next/utils.py @@ -2,7 +2,26 @@ # SPDX-License-Identifier: MPL-2.0 import json -from typing import Dict, Union +from typing import Dict, Union, Callable +from typegraph_next.effects import EffectType + + +def serialize_injection( + source: str, + value: Union[any, Dict[EffectType, any]], + value_mapper: Callable[[any], any] = lambda x: x, +): + if ( + isinstance(value, dict) + and len(value) > 0 + and all(isinstance(k, EffectType) for k in value.keys()) + ): + value_per_effect = { + str(k.name.lower()): value_mapper(v) for k, v in value.items() + } + return json.dumps({"source": source, "data": value_per_effect}) + + return json.dumps({"source": source, "data": {"value": value_mapper(value)}}) def serialize_record_values(obj: Union[Dict[str, any], None]):