diff --git a/docs/docs/03-language-reference.md b/docs/docs/03-language-reference.md index 12206349a7e..35ab2ab090e 100644 --- a/docs/docs/03-language-reference.md +++ b/docs/docs/03-language-reference.md @@ -286,6 +286,12 @@ str.fromJson(jsonNumber); // RUNTIME ERROR: unable to parse number `123` as num.fromJson(Json "\"hello\""); // RUNTIME ERROR: unable to parse string "hello" as a number ``` +For each `fromJson()`, there is a `tryFromJson()` method which returns an optional `T?` which +indicates if parsing was successful or not: +```js +let s = str.tryFromJson(myJson) ?? "invalid string"; +`````` + ##### 1.1.4.6 Mutability To define a mutable JSON container, use the `MutJson` type: @@ -335,7 +341,31 @@ Json.delete(immutObj, "hello"); // ^^^^^^^^^ expected `JsonMut` ``` -##### 1.1.4.7 Serialization +##### 1.1.4.7 Assignment to user-defined structs +All [structs](#31-structs) also have a `fromJson()` method that can be used to parse `Json` into a +struct: +```js +struct Contact { + first: str; + last: str; + phone: str?; +} + +let j = Json { first: "Wing", last: "Lyly" }; +let myContact = Contact.fromJson(j); +assert(myContact.first == "Wing"); +``` +When a `Json` is parsed into a struct, the schema will be validated to ensure the result is +type-safe: +```js +let p = Json { first: "Wing", phone: 1234 }; +Contact.fromJson(p); +// RUNTIME ERROR: unable to parse Contact: +// - field "last" is required and missing +// - field "phone" is expected to be a string, got number. +``` + +##### 1.1.4.8 Serialization The `Json.stringify(j: Json): str` static method can be used to serialize a `Json` as a string ([JSON.stringify](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify)): @@ -360,7 +390,7 @@ let boom = num.fromJson(j.get("boom")); let o = Json.tryParse("xxx") ?? Json [1,2,3]; ``` -##### 1.1.4.8 Logging +##### 1.1.4.9 Logging A `Json` value can be logged using `log()`, in which case it will be pretty-formatted: @@ -378,13 +408,12 @@ my object is: { } ``` -#### 1.1.4.9 Roadmap +#### 1.1.4.10 Roadmap The following features are not yet implemented, but we are planning to add them in the future: * Array/Set/Map.fromJson() - see https://github.com/winglang/wing/issues/1796 to track. * Json.entries() - see https://github.com/winglang/wing/issues/3142 to track. -* Schema validation and assignment to struct - see https://github.com/winglang/wing/issues/3139 to track. * Equality, diff and patch - see https://github.com/winglang/wing/issues/3140 to track. [`▲ top`][top] diff --git a/docs/docs/04-standard-library/02-std/api-reference.md b/docs/docs/04-standard-library/02-std/api-reference.md index 068d1bb1993..6a3b4cea1fe 100644 --- a/docs/docs/04-standard-library/02-std/api-reference.md +++ b/docs/docs/04-standard-library/02-std/api-reference.md @@ -2068,6 +2068,50 @@ The length of the string. --- +### Struct + +Shared behavior for all structs. + + +#### Static Functions + +| **Name** | **Description** | +| --- | --- | +| fromJson | Converts a Json to a Struct. | +| tryFromJson | Converts a Json to a Struct, returning nil if the Json is not valid. | + +--- + +##### `fromJson` + +```wing +Struct.fromJson(json: Json); +``` + +Converts a Json to a Struct. + +###### `json`Required + +- *Type:* Json + +--- + +##### `tryFromJson` + +```wing +Struct.tryFromJson(json: Json); +``` + +Converts a Json to a Struct, returning nil if the Json is not valid. + +###### `json`Required + +- *Type:* Json + +--- + + + ## Structs ### DatetimeComponents diff --git a/examples/tests/error/struct_from_json_1.w b/examples/tests/error/struct_from_json_1.w new file mode 100644 index 00000000000..5c63c78626e --- /dev/null +++ b/examples/tests/error/struct_from_json_1.w @@ -0,0 +1,13 @@ +// Note that this test has to be alone because it needs to compile successfully and fail at preflight. +// If it is run with other tests, subsequent failures will be ignored in snapshot. + +struct Person { + name: str; + age: num; +} + +let j = {name: "cool", age: "not a number"}; + +Person.fromJson(j); +// ^ ERROR: unable to parse Person: +// - instance.age is not of a type(s) number diff --git a/examples/tests/error/struct_from_json_2.w b/examples/tests/error/struct_from_json_2.w new file mode 100644 index 00000000000..bf0886e79b2 --- /dev/null +++ b/examples/tests/error/struct_from_json_2.w @@ -0,0 +1,25 @@ +// Note that this test has to be alone because it needs to compile successfully and fail at preflight. +// If it is run with other tests, subsequent failures will be ignored in snapshot. + +struct Person { + name: str; + age: num; +} + +struct Advisor extends Person { + id: str; +} + +struct Student extends Person { + advisor: Advisor; +} + +let missingAdvisor = { + name: "cool", + age: "not a number" +}; + +Student.fromJson(missingAdvisor); +// ^ ERROR: unable to parse Student: +// - instance.age is not of a type(s) number +// - instance requires property "advisor" \ No newline at end of file diff --git a/examples/tests/error/struct_from_json_3.w b/examples/tests/error/struct_from_json_3.w new file mode 100644 index 00000000000..97ca8911925 --- /dev/null +++ b/examples/tests/error/struct_from_json_3.w @@ -0,0 +1,29 @@ +// Note that this test has to be alone because it needs to compile successfully and fail at preflight. +// If it is run with other tests, subsequent failures will be ignored in snapshot. + +struct Person { + name: str; + age: num; +} + +struct Advisor extends Person { + id: str; +} + +struct Student extends Person { + advisors: Array; +} + +let invalidAdvisorInArray = { + name: "cool", + age: "not a number", + advisors: [ + {id: "advisor1", name: "Bob", age: 34}, + {id: 10, name: "Jacob", age: 45} + ] +}; + +Student.fromJson(invalidAdvisorInArray); +// ^ ERROR: unable to parse Student: +// - instance.age is not of a type(s) number +// - instance.advisors[1].id is not of a type(s) string diff --git a/examples/tests/error/struct_from_json_4.w b/examples/tests/error/struct_from_json_4.w new file mode 100644 index 00000000000..fd2cc152a23 --- /dev/null +++ b/examples/tests/error/struct_from_json_4.w @@ -0,0 +1,29 @@ +// Note that this test has to be alone because it needs to compile successfully and fail at preflight. +// If it is run with other tests, subsequent failures will be ignored in snapshot. + +struct Person { + name: str; + age: num; +} + +struct Advisor extends Person { + id: str; +} + +struct Student extends Person { + advisors: Set; // <== Using Set instead of Array +} + +// Try adding two of the same adivsor +let invalidAdvisorInArray = { + name: "cool", + age: 22, + advisors: [ + {id: "advisor1", name: "Bob", age: 34}, + {id: "advisor1", name: "Bob", age: 34}, + ] +}; + +Student.fromJson(invalidAdvisorInArray); +// ^ ERROR: unable to parse Student: +// - instance.advisors contains duplicate item diff --git a/examples/tests/error/struct_from_json_5.w b/examples/tests/error/struct_from_json_5.w new file mode 100644 index 00000000000..4ee746f3e57 --- /dev/null +++ b/examples/tests/error/struct_from_json_5.w @@ -0,0 +1,18 @@ +// Note that this test has to be alone because it needs to compile successfully and fail at preflight. +// If it is run with other tests, subsequent failures will be ignored in snapshot. + +struct Foo { + names: Map; +} + +let jFoo = { + names: { + a: "Amanda", + b: "Barry", + c: 10 + } +}; + +Foo.fromJson(jFoo); +// ^ ERROR: unable to parse Foo: +// - instance.names.c is not of a type(s) string diff --git a/examples/tests/invalid/struct_json_conversion.w b/examples/tests/invalid/struct_json_conversion.w new file mode 100644 index 00000000000..1ef683c2fde --- /dev/null +++ b/examples/tests/invalid/struct_json_conversion.w @@ -0,0 +1,23 @@ +bring cloud; + +struct A { + a: str; + b: cloud.Bucket; +} + +A.fromJson({}); +//^^^^^^^^ Struct "A" contains field "b" which cannot be represented in Json + +struct B { + a: A; +} + +B.fromJson({}); +//^^^^^^^^ Struct "B" contains field "a" which cannot be represented in Json + +struct C extends A { + c: num; +} + +C.fromJson({}); +//^^^^^^^^ Struct "C" contains field "b" which cannot be represented in Json \ No newline at end of file diff --git a/examples/tests/valid/struct_from_json.w b/examples/tests/valid/struct_from_json.w new file mode 100644 index 00000000000..d3db2b0b4a9 --- /dev/null +++ b/examples/tests/valid/struct_from_json.w @@ -0,0 +1,239 @@ +// TODO: https://github.com/winglang/wing/issues/3792 +// bring cloud; +// let j = { public: false }; +// let x = cloud.BucketProps.fromJson(j); + +// simple case +struct Foo { + f: str; +} + +let jFoo = { f: "bar"}; +assert(Foo.fromJson(jFoo).f == "bar"); + +// optionality +struct Foosible { + f: str?; +} + +let jFoosible = {}; // Not there +let jFoosible2 = {f: "bar"}; + +if let f = Foosible.fromJson(jFoosible).f { + assert(false); // Should not happen +} + +if let f = Foosible.fromJson(jFoosible2).f { + assert(f == "bar"); +} else { + assert(false); // Should not happen +} + +// Getting fancy now +struct Bar extends Foo { + b: num; +} + +let jBar = { f: "bar", b: 10}; + +let b = Bar.fromJson(jBar); +assert(b.f == "bar"); +assert(b.b == 10); + +// Lets go full out +struct Date { + month: num; + day: num; + year: num; +} + +struct Person { + firstName: str; + lastName: str; + dob: Date; +} + +struct Advisor extends Person { + employeeID: str; +} + +struct Course { + name: str; + credits: num; +} + +struct CourseResults { + course: Course; + grade: str; + dateTaken: Date; +} + +struct Student extends Person { + enrolled: bool; + schoolId: str; + advisor: Advisor?; + enrolledCourses: Set?; + coursesTaken: Array?; + additionalData: Json?; +} + +// Student with no advisor and no empty course list +let jStudent1 = { + firstName: "John", + lastName: "Smith", + enrolled: true, + schoolId: "s1-xyz", + dob: { month: 10, day: 10, year: 2005 }, + enrolledCourses: [] +}; + +let student1 = Student.fromJson(jStudent1); +assert(student1.firstName == "John"); +assert(student1.lastName == "Smith"); +assert(student1.enrolled); +assert(student1.schoolId == "s1-xyz"); +assert(student1.dob.month == 10); +assert(student1.dob.day == 10); +assert(student1.dob.year == 2005); + +// Student with an advisor and several courses +let jStudent2 = { + advisor: { + firstName: "Tom", + lastName: "Baker", + dob: { month: 1, day: 1, year: 1983 }, + employeeID: "emp123" + }, + firstName: "Sally", + lastName: "Reynolds", + enrolled: false, + schoolId: "s2-xyz", + dob: { month: 5, day: 31, year: 1987}, + enrolledCourses: [ + { name: "COMP 101", credits: 2 }, + { name: "COMP 121", credits: 4 }, + ], + coursesTaken: [ + {grade: "F", dateTaken: { month: 5, day: 10, year: 2021 }, course: { name: "COMP 101", credits: 2 }}, + {grade: "D", dateTaken: { month: 5, day: 10, year: 2021 }, course: { name: "COMP 121", credits: 4 }}, + ] +}; + +let student2 = Student.fromJson(jStudent2); +assert(student2.firstName == "Sally"); +assert(student2.lastName == "Reynolds"); +assert(!student2.enrolled); +assert(student2.schoolId == "s2-xyz"); +assert(student2.dob.month == 5); +assert(student2.dob.day == 31); +assert(student2.dob.year == 1987); + +if let enrolledCourses = student2.enrolledCourses { + let courses = enrolledCourses.toArray(); + let s2Course1 = courses.at(0); + let s2Course2 = courses.at(1); + + assert(s2Course1.name == "COMP 101"); + assert(s2Course1.credits == 2); + assert(s2Course2.name == "COMP 121"); + assert(s2Course2.credits == 4); +} else { + assert(false); // It should never hit this +} + +// Create a student that has additional data (Json) +let jStudent3 = { + enrolled: false, + schoolId: "w/e", + firstName: student2.firstName, + lastName: student2.lastName, + dob: { + month: 1, day: 1, year: 1959 + }, + additionalData: { + notes: "wow such notes", + legacy: false, + emergencyContactsNumbers: [ + "123-345-9928", + ] + } +}; + +let student3 = Student.fromJson(jStudent3); + +if let additionalData = student3.additionalData { + let notes = additionalData.get("notes"); + assert(notes == "wow such notes"); +} else { + assert(false); // shouldnt happen +} + + +// Create student missing required field +let invalidStudent = { + firstName: "I dont have", + lastName: "Any other info" +}; + +if let student = Student.tryFromJson(invalidStudent) { + assert(false); // should not have been able to create student +} else { + assert(true); +} + +// Use tryFromJson on a valid student + +if let student = Student.tryFromJson(jStudent2) { + assert(student.firstName == "Sally"); + assert(student.lastName == "Reynolds"); + assert(!student.enrolled); + assert(student.schoolId == "s2-xyz"); + assert(student.dob.month == 5); + assert(student.dob.day == 31); + assert(student.dob.year == 1987); +} else { + assert(false); // Should not happen +} + +test "flight school student :)" { + let jStudent3 = { + firstName: "struct", + lastName: "greatest", + enrolled: true, + schoolId: "s3-inflight", + dob: { month: 4, day: 1, year: 1999}, + coursesTaken: [ + {grade: "B", dateTaken: { month: 5, day: 10, year: 2021 }, course: { name: "COMP 101", credits: 2 }}, + {grade: "A", dateTaken: { month: 5, day: 10, year: 2021 }, course: { name: "COMP 121", credits: 4 }}, + ] + }; + let studentInflight1 = Student.fromJson(jStudent3); + assert(studentInflight1.firstName == "struct"); + assert(studentInflight1.lastName == "greatest"); + assert(studentInflight1.enrolled); + assert(studentInflight1.schoolId == "s3-inflight"); + assert(studentInflight1.dob.month == 4); + assert(studentInflight1.dob.day == 1); + assert(studentInflight1.dob.year == 1999); + + if let coursesTaken = studentInflight1.coursesTaken { + let course1 = coursesTaken.at(0); + let course2 = coursesTaken.at(1); + + assert(course1.grade == "B"); + assert(course2.grade == "A"); + } else { + assert(false); // should not happen + } +} + +test "lifting a student" { + let studentInflight1 = Student.fromJson(jStudent1); + assert(studentInflight1.firstName == "John"); + assert(studentInflight1.lastName == "Smith"); + assert(studentInflight1.enrolled); + assert(studentInflight1.schoolId == "s1-xyz"); + assert(studentInflight1.dob.month == 10); + assert(studentInflight1.dob.day == 10); + assert(studentInflight1.dob.year == 2005); +} \ No newline at end of file diff --git a/libs/wingc/src/jsify.rs b/libs/wingc/src/jsify.rs index 88aa441a085..4216affb148 100644 --- a/libs/wingc/src/jsify.rs +++ b/libs/wingc/src/jsify.rs @@ -17,8 +17,8 @@ use std::{ use crate::{ ast::{ ArgList, BinaryOperator, BringSource, CalleeKind, Class as AstClass, Expr, ExprKind, FunctionBody, - FunctionDefinition, InterpolatedStringPart, Literal, NewExpr, Phase, Reference, Scope, Stmt, StmtKind, Symbol, - TypeAnnotationKind, UnaryOperator, UserDefinedType, + FunctionDefinition, InterpolatedStringPart, Literal, NewExpr, Phase, Reference, Scope, Stmt, StmtKind, StructField, + Symbol, TypeAnnotationKind, UnaryOperator, UserDefinedType, }, comp_ctx::{CompilationContext, CompilationPhase}, dbg_panic, debug, @@ -238,7 +238,7 @@ impl<'a> JSifier<'a> { property, optional_accessor, } => self.jsify_expression(object, ctx) + (if *optional_accessor { "?." } else { "." }) + &property.to_string(), - Reference::TypeReference(udt) => self.jsify_type(&TypeAnnotationKind::UserDefined(udt.clone())), + Reference::TypeReference(udt) => self.jsify_user_defined_type(&udt), Reference::TypeMember { typeobject, property } => { let typename = self.jsify_expression(typeobject, ctx); typename + "." + &property.to_string() @@ -283,10 +283,83 @@ impl<'a> JSifier<'a> { } } - fn jsify_type(&self, typ: &TypeAnnotationKind) -> String { + fn jsify_type(&self, typ: &TypeAnnotationKind) -> Option { match typ { - TypeAnnotationKind::UserDefined(t) => self.jsify_user_defined_type(&t), - _ => todo!(), + TypeAnnotationKind::UserDefined(t) => Some(self.jsify_user_defined_type(&t)), + TypeAnnotationKind::String => Some("string".to_string()), + TypeAnnotationKind::Number => Some("number".to_string()), + TypeAnnotationKind::Bool => Some("boolean".to_string()), + TypeAnnotationKind::Array(t) => { + if let Some(inner) = self.jsify_type(&t.kind) { + Some(format!("{}[]", inner)) + } else { + None + } + } + TypeAnnotationKind::Optional(t) => { + if let Some(inner) = self.jsify_type(&t.kind) { + Some(format!("{}?", inner)) + } else { + None + } + } + _ => None, + } + } + + // This helper determines what requirement and dependency to add to the struct schema based + // on the type annotation of the field. + // I.E. if a struct has a field named "foo" with a type "OtherStruct", then we want to add + // the field "foo" as a required and the struct "OtherStruct" as a dependency. so the result is + // a tuple (required, dependency) + fn extract_struct_field_schema_dependency( + &self, + typ: &TypeAnnotationKind, + field_name: &String, + ) -> (Option, Option) { + match typ { + TypeAnnotationKind::UserDefined(udt) => (Some(field_name.clone()), Some(udt.root.name.clone())), + TypeAnnotationKind::Array(t) | TypeAnnotationKind::Set(t) | TypeAnnotationKind::Map(t) => { + self.extract_struct_field_schema_dependency(&t.kind, field_name) + } + TypeAnnotationKind::Optional(t) => { + let deps = self.extract_struct_field_schema_dependency(&t.kind, field_name); + // We never want to add an optional to the required block + (None, deps.1) + } + _ => (Some(field_name.clone()), None), + } + } + + fn jsify_struct_field_to_json_schema_type(&self, typ: &TypeAnnotationKind) -> String { + match typ { + TypeAnnotationKind::Bool | TypeAnnotationKind::Number | TypeAnnotationKind::String => { + format!("type: \"{}\"", self.jsify_type(typ).unwrap()) + } + TypeAnnotationKind::UserDefined(udt) => { + format!("\"$ref\": \"#/$defs/{}\"", udt.root.name) + } + TypeAnnotationKind::Json => "type: \"object\"".to_string(), + TypeAnnotationKind::Map(t) => { + let map_type = self.jsify_type(&t.kind); + // Ensure all keys are of some type + format!( + "type: \"object\", patternProperties: {{ \".*\": {{ type: \"{}\" }} }}", + map_type.unwrap_or("null".to_string()) + ) + } + TypeAnnotationKind::Array(t) | TypeAnnotationKind::Set(t) => { + format!( + "type: \"array\", {} items: {{ {} }}", + match typ { + TypeAnnotationKind::Set(_) => "uniqueItems: true,".to_string(), + _ => "".to_string(), + }, + self.jsify_struct_field_to_json_schema_type(&t.kind) + ) + } + TypeAnnotationKind::Optional(t) => self.jsify_struct_field_to_json_schema_type(&t.kind), + _ => "type: \"null\"".to_string(), } } @@ -466,7 +539,10 @@ impl<'a> JSifier<'a> { ExprKind::Reference(Reference::Identifier(_)) => "global".to_string(), ExprKind::Reference(Reference::InstanceMember { object, .. }) => { self.jsify_expression(&object, ctx) - } + }, + ExprKind::Reference(Reference::TypeMember { .. }) => { + expr_string.clone().split(".").next().unwrap_or("").to_string() + }, _ => expr_string, } CalleeKind::SuperCall{..} => @@ -594,6 +670,122 @@ impl<'a> JSifier<'a> { } } + pub fn jsify_struct_properties(&self, fields: &Vec, extends: &Vec) -> CodeMaker { + let mut code = CodeMaker::default(); + + // Any parents we need to get their properties + for e in extends { + code.line(format!( + "...require(\"{}\")().jsonSchema().properties,", + struct_filename(&e.root.name) + )) + } + + for field in fields { + code.line(format!( + "{}: {{ {} }},", + field.name.name, + self.jsify_struct_field_to_json_schema_type(&field.member_type.kind) + )); + } + + code + } + + pub fn jsify_struct( + &self, + name: &Symbol, + fields: &Vec, + extends: &Vec, + _env: &SymbolEnv, + ) -> CodeMaker { + // To allow for struct validation at runtime this will generate a JS class that has a static + // getValidator method that will create a json schema validator. + let mut code = CodeMaker::default(); + + code.open("module.exports = function(stdStruct, fromInline) {".to_string()); + code.open(format!("class {} {{", name)); + + // create schema + let mut required: Vec = vec![]; // fields that are required + let mut dependencies: Vec = vec![]; // schemas that need added to validator + + code.open("static jsonSchema() {".to_string()); + code.open("return {"); + code.line(format!("id: \"/{}\",", name)); + code.line("type: \"object\",".to_string()); + + code.open("properties: {"); + + code.add_code(self.jsify_struct_properties(fields, extends)); + + // determine which fields are required, and which schemas need to be added to validator + for field in fields { + let dep = self.extract_struct_field_schema_dependency(&field.member_type.kind, &field.name.name); + if let Some(req) = dep.0 { + required.push(req); + } + if let Some(dep) = dep.1 { + dependencies.push(dep); + } + } + code.close("},"); + + // Add all required field names to schema + code.open("required: ["); + for name in required { + code.line(format!("\"{}\",", name)); + } + + // pull in all required fields from parent structs + for e in extends { + code.line(format!( + "...require(\"{}\")().jsonSchema().required,", + struct_filename(&e.root.name) + )); + } + + code.close("],"); + + // create definitions for sub schemas + code.open("$defs: {"); + for dep in &dependencies { + code.line(format!( + "\"{}\": {{ type: \"object\", \"properties\": require(\"{}\")().jsonSchema().properties }},", + dep, + struct_filename(&dep) + )); + } + for e in extends { + code.line(format!( + "...require(\"{}\")().jsonSchema().$defs,", + struct_filename(&e.root.name) + )); + } + code.close("}"); + + code.close("}"); + code.close("}"); + + // create _validate() function + code.open("static fromJson(obj) {"); + code.line("return stdStruct._validate(obj, this.jsonSchema())"); + code.close("}"); + + // create _toInflightType function that just requires the generated struct file + code.open("static _toInflightType(context) {".to_string()); + code.line(format!( + "return fromInline(`require(\"{}\")(${{ context._lift(stdStruct) }})`);", + struct_filename(&name.name) + )); + code.close("}"); + code.close("}"); + code.line(format!("return {};", name)); + code.close("};"); + + code + } + fn jsify_statement(&self, env: &SymbolEnv, statement: &Stmt, ctx: &mut JSifyContext) -> CodeMaker { CompilationContext::set(CompilationPhase::Jsifying, &statement.span); match &statement.kind { @@ -771,9 +963,21 @@ impl<'a> JSifier<'a> { // This is a no-op in JS CodeMaker::default() } - StmtKind::Struct { .. } => { - // This is a no-op in JS - CodeMaker::default() + StmtKind::Struct { name, fields, extends } => { + let mut code = self.jsify_struct(name, fields, extends, env); + // Emits struct class file + self.emit_struct_file(name, code, ctx); + + // Reset the code maker for code to be inserted in preflight.js + code = CodeMaker::default(); + code.line(format!( + "const {} = require(\"{}\")({}.std.Struct, {}.core.NodeJsCode.fromInline);", + name, + struct_filename(&name.name), + STDLIB, + STDLIB + )); + code } StmtKind::Enum { name, values } => { let mut code = CodeMaker::default(); @@ -1158,6 +1362,19 @@ impl<'a> JSifier<'a> { class_code } + fn emit_struct_file(&self, name: &Symbol, struct_code: CodeMaker, _ctx: &mut JSifyContext) { + let mut code = CodeMaker::default(); + code.add_code(struct_code); + match self + .output_files + .borrow_mut() + .add_file(struct_filename(&name.name), code.to_string()) + { + Ok(()) => {} + Err(err) => report_diagnostic(err.into()), + } + } + fn emit_inflight_file(&self, class: &AstClass, inflight_class_code: CodeMaker, ctx: &mut JSifyContext) { let name = &class.name.name; let mut code = CodeMaker::default(); @@ -1345,6 +1562,14 @@ fn get_public_symbols(scope: &Scope) -> Vec { symbols } +fn inflight_filename(class: &AstClass) -> String { + format!("./inflight.{}.js", class.name.name) +} + +fn struct_filename(s: &String) -> String { + format!("./{}.Struct.js", s) +} + fn lookup_span(span: &WingSpan, files: &Files) -> String { let source = files .get_file(&span.file_id) diff --git a/libs/wingc/src/lib.rs b/libs/wingc/src/lib.rs index 1a3132ae05d..7f2eb2eb070 100644 --- a/libs/wingc/src/lib.rs +++ b/libs/wingc/src/lib.rs @@ -90,6 +90,7 @@ const WINGSDK_STRING: &'static str = "std.String"; const WINGSDK_JSON: &'static str = "std.Json"; const WINGSDK_MUT_JSON: &'static str = "std.MutJson"; const WINGSDK_RESOURCE: &'static str = "std.Resource"; +const WINGSDK_STRUCT: &'static str = "std.Struct"; const WINGSDK_TEST_CLASS_NAME: &'static str = "Test"; const CONSTRUCT_BASE_CLASS: &'static str = "constructs.Construct"; diff --git a/libs/wingc/src/type_check.rs b/libs/wingc/src/type_check.rs index 3ba3ac2f903..2bdd1036546 100644 --- a/libs/wingc/src/type_check.rs +++ b/libs/wingc/src/type_check.rs @@ -17,7 +17,7 @@ use crate::visit_types::{VisitType, VisitTypeMut}; use crate::{ dbg_panic, debug, WINGSDK_ARRAY, WINGSDK_ASSEMBLY_NAME, WINGSDK_BRINGABLE_MODULES, WINGSDK_DURATION, WINGSDK_JSON, WINGSDK_MAP, WINGSDK_MUT_ARRAY, WINGSDK_MUT_JSON, WINGSDK_MUT_MAP, WINGSDK_MUT_SET, WINGSDK_RESOURCE, WINGSDK_SET, - WINGSDK_STD_MODULE, WINGSDK_STRING, + WINGSDK_STD_MODULE, WINGSDK_STRING, WINGSDK_STRUCT, }; use derivative::Derivative; use duplicate::duplicate_item; @@ -1115,6 +1115,25 @@ impl TypeRef { _ => false, } } + + // This is slightly different than is_json_legal_value in that its purpose + // is to determine if a type can be represented in JSON before we allow users to attempt + // convert from Json + pub fn has_json_representation(&self) -> bool { + match &**self { + Type::Struct(s) => { + // check all its fields are json compatible + for (_, field) in s.fields(true) { + if !field.has_json_representation() { + return false; + } + } + true + } + Type::Optional(t) | Type::Array(t) | Type::Set(t) | Type::Map(t) => t.has_json_representation(), + _ => self.is_json_legal_value(), + } + } } impl Subtype for TypeRef { @@ -4270,6 +4289,32 @@ impl<'a> TypeChecker<'a> { ) } } + Type::Struct(ref s) => { + const FROM_JSON: &str = "fromJson"; + const TRY_FROM_JSON: &str = "tryFromJson"; + + if property.name == FROM_JSON || property.name == TRY_FROM_JSON { + // we need to validate that only structs with all valid json fields can have a fromJson method + for (name, field) in s.fields(true) { + if !field.has_json_representation() { + self.spanned_error_with_var( + property, + format!( + "Struct \"{}\" contains field \"{}\" which cannot be represented in Json", + type_, name + ), + ); + return (self.make_error_variable_info(), Phase::Independent); + } + } + } + let lookup = env.lookup(&s.name, None); + let type_ = lookup.unwrap().as_type().unwrap(); + + let new_class = self.hydrate_class_type_arguments(env, WINGSDK_STRUCT, vec![type_]); + let v = self.get_property_from_class_like(new_class.as_class().unwrap(), property, true); + (v, Phase::Independent) + } Type::Class(ref c) => match c.env.lookup(&property, None) { Some(SymbolKind::Variable(v)) => { if let VariableKind::StaticMember = v.kind { @@ -4305,8 +4350,8 @@ impl<'a> TypeChecker<'a> { ) -> VariableInfo { match *instance_type { Type::Optional(t) => self.resolve_variable_from_instance_type(t, property, env, _object), - Type::Class(ref class) => self.get_property_from_class_like(class, property), - Type::Interface(ref interface) => self.get_property_from_class_like(interface, property), + Type::Class(ref class) => self.get_property_from_class_like(class, property, false), + Type::Interface(ref interface) => self.get_property_from_class_like(interface, property, false), Type::Anything => VariableInfo { name: property.clone(), type_: instance_type, @@ -4319,27 +4364,27 @@ impl<'a> TypeChecker<'a> { // Lookup wingsdk std types, hydrating generics if necessary Type::Array(t) => { let new_class = self.hydrate_class_type_arguments(env, WINGSDK_ARRAY, vec![t]); - self.get_property_from_class_like(new_class.as_class().unwrap(), property) + self.get_property_from_class_like(new_class.as_class().unwrap(), property, false) } Type::MutArray(t) => { let new_class = self.hydrate_class_type_arguments(env, WINGSDK_MUT_ARRAY, vec![t]); - self.get_property_from_class_like(new_class.as_class().unwrap(), property) + self.get_property_from_class_like(new_class.as_class().unwrap(), property, false) } Type::Set(t) => { let new_class = self.hydrate_class_type_arguments(env, WINGSDK_SET, vec![t]); - self.get_property_from_class_like(new_class.as_class().unwrap(), property) + self.get_property_from_class_like(new_class.as_class().unwrap(), property, false) } Type::MutSet(t) => { let new_class = self.hydrate_class_type_arguments(env, WINGSDK_MUT_SET, vec![t]); - self.get_property_from_class_like(new_class.as_class().unwrap(), property) + self.get_property_from_class_like(new_class.as_class().unwrap(), property, false) } Type::Map(t) => { let new_class = self.hydrate_class_type_arguments(env, WINGSDK_MAP, vec![t]); - self.get_property_from_class_like(new_class.as_class().unwrap(), property) + self.get_property_from_class_like(new_class.as_class().unwrap(), property, false) } Type::MutMap(t) => { let new_class = self.hydrate_class_type_arguments(env, WINGSDK_MUT_MAP, vec![t]); - self.get_property_from_class_like(new_class.as_class().unwrap(), property) + self.get_property_from_class_like(new_class.as_class().unwrap(), property, false) } Type::Json => self.get_property_from_class_like( env @@ -4351,6 +4396,7 @@ impl<'a> TypeChecker<'a> { .as_class() .unwrap(), property, + false, ), Type::MutJson => self.get_property_from_class_like( env @@ -4362,6 +4408,7 @@ impl<'a> TypeChecker<'a> { .as_class() .unwrap(), property, + false, ), Type::String => self.get_property_from_class_like( env @@ -4373,6 +4420,7 @@ impl<'a> TypeChecker<'a> { .as_class() .unwrap(), property, + false, ), Type::Duration => self.get_property_from_class_like( env @@ -4384,8 +4432,9 @@ impl<'a> TypeChecker<'a> { .as_class() .unwrap(), property, + false, ), - Type::Struct(ref s) => self.get_property_from_class_like(s, property), + Type::Struct(ref s) => self.get_property_from_class_like(s, property, true), _ => { self .spanned_error_with_var(property, "Property not found".to_string()) @@ -4395,11 +4444,19 @@ impl<'a> TypeChecker<'a> { } /// Get's the type of an instance variable in a class - fn get_property_from_class_like(&mut self, class: &impl ClassLike, property: &Symbol) -> VariableInfo { + fn get_property_from_class_like( + &mut self, + class: &impl ClassLike, + property: &Symbol, + allow_static: bool, + ) -> VariableInfo { let lookup_res = class.get_env().lookup_ext(property, None); if let LookupResult::Found(field, _) = lookup_res { let var = field.as_variable().expect("Expected property to be a variable"); if let VariableKind::StaticMember = var.kind { + if allow_static { + return var.clone(); + } self .spanned_error_with_var( property, diff --git a/libs/wingsdk/.projen/deps.json b/libs/wingsdk/.projen/deps.json index 41dbe6967d5..663b3b5cc13 100644 --- a/libs/wingsdk/.projen/deps.json +++ b/libs/wingsdk/.projen/deps.json @@ -268,6 +268,10 @@ "name": "ioredis", "type": "bundled" }, + { + "name": "jsonschema", + "type": "bundled" + }, { "name": "mime-types", "type": "bundled" diff --git a/libs/wingsdk/.projenrc.ts b/libs/wingsdk/.projenrc.ts index 9c4422764b7..54aa42008d8 100644 --- a/libs/wingsdk/.projenrc.ts +++ b/libs/wingsdk/.projenrc.ts @@ -103,6 +103,7 @@ const project = new cdk.JsiiProject({ "cron-parser", // shared client dependencies "ioredis", + "jsonschema", ], devDeps: [ `@cdktf/provider-aws@^15.0.0`, // only for testing Wing plugins diff --git a/libs/wingsdk/package.json b/libs/wingsdk/package.json index 22360143c83..5325b03466e 100644 --- a/libs/wingsdk/package.json +++ b/libs/wingsdk/package.json @@ -103,6 +103,7 @@ "esbuild-wasm": "^0.18.5", "express": "^4.18.2", "ioredis": "^5.3.1", + "jsonschema": "^1.4.1", "mime-types": "^2.1.35", "nanoid": "^3.3.6", "safe-stable-stringify": "^2.4.3", @@ -133,6 +134,7 @@ "esbuild-wasm", "express", "ioredis", + "jsonschema", "mime-types", "nanoid", "safe-stable-stringify", diff --git a/libs/wingsdk/src/std/index.ts b/libs/wingsdk/src/std/index.ts index f135327b652..20a20c23409 100644 --- a/libs/wingsdk/src/std/index.ts +++ b/libs/wingsdk/src/std/index.ts @@ -10,5 +10,6 @@ export * from "./range"; export * from "./resource"; export * from "./set"; export * from "./string"; +export * from "./struct"; export * from "./test"; export * from "./test-runner"; diff --git a/libs/wingsdk/src/std/struct.ts b/libs/wingsdk/src/std/struct.ts new file mode 100644 index 00000000000..4398c77b62b --- /dev/null +++ b/libs/wingsdk/src/std/struct.ts @@ -0,0 +1,63 @@ +import { Validator } from "jsonschema"; +import { T1 } from "./generics"; +import { Json } from "./json"; +import { Code, InflightClient } from "../core"; + +/** + * Shared behavior for all structs + * + * @typeparam T1 + */ +export class Struct { + /** + * @internal + */ + public static _toInflightType(): Code { + return InflightClient.forType(__filename, this.name); + } + + /** + * Converts a Json to a Struct + * + * @macro ($self$.fromJson($args$)) + */ + public static fromJson(json: Json): T1 { + json; + throw new Error("Macro"); + } + + /** + * Converts a Json to a Struct, returning nil if the Json is not valid + * + * @macro (() => { try { return $self$.fromJson($args$); } catch { return undefined; }})(); + */ + public static tryFromJson(json: Json): T1 | undefined { + json; + throw new Error("Macro"); + } + + /** + * Validates a Json object against a schema + * + * The expected schema format: https://json-schema.org/ + * + * @param obj Json object to validate + * @param schema schema to validate against + * + * @internal + */ + public static _validate(obj: Json, schema: any): Json { + const validator = new Validator(); + const result = validator.validate(obj, schema); + if (result.errors.length > 0) { + throw new Error( + `unable to parse ${schema.id.replace("/", "")}:\n ${result.errors.join( + "\n- " + )}` + ); + } + return obj; + } + + private constructor() {} +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c788f95d0bc..839a6eb52cc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1143,6 +1143,9 @@ importers: ioredis: specifier: ^5.3.1 version: 5.3.1 + jsonschema: + specifier: ^1.4.1 + version: 1.4.1 mime-types: specifier: ^2.1.35 version: 2.1.35 diff --git a/tools/hangar/__snapshots__/error.ts.snap b/tools/hangar/__snapshots__/error.ts.snap index 6b3922f042a..d0182319a98 100644 --- a/tools/hangar/__snapshots__/error.ts.snap +++ b/tools/hangar/__snapshots__/error.ts.snap @@ -109,6 +109,98 @@ exports[`string_from_json.w 1`] = ` +Tests 1 failed (1) +Test Files 1 failed (1) +Duration " +`; + +exports[`struct_from_json_1.w 1`] = ` +"ERROR: unable to parse Person: + instance.age is not of a type(s) number + +../../../examples/tests/error/target/test/struct_from_json_1.wsim.[REDACTED].tmp/.wing/preflight.js:9 + const Person = require(\\"./Person.Struct.js\\")($stdlib.std.Struct, $stdlib.core.NodeJsCode.fromInline); + const j = ({\\"name\\": \\"cool\\",\\"age\\": \\"not a number\\"}); +>> (Person.fromJson(j)); + } + } + + + +Tests 1 failed (1) +Test Files 1 failed (1) +Duration " +`; + +exports[`struct_from_json_2.w 1`] = ` +"ERROR: unable to parse Student: + instance.age is not of a type(s) number +- instance requires property \\"advisor\\" + +../../../examples/tests/error/target/test/struct_from_json_2.wsim.[REDACTED].tmp/.wing/preflight.js:11 + const Student = require(\\"./Student.Struct.js\\")($stdlib.std.Struct, $stdlib.core.NodeJsCode.fromInline); + const missingAdvisor = ({\\"name\\": \\"cool\\",\\"age\\": \\"not a number\\"}); +>> (Student.fromJson(missingAdvisor)); + } + } + + + +Tests 1 failed (1) +Test Files 1 failed (1) +Duration " +`; + +exports[`struct_from_json_3.w 1`] = ` +"ERROR: unable to parse Student: + instance.age is not of a type(s) number +- instance.advisors[1].id is not of a type(s) string + +../../../examples/tests/error/target/test/struct_from_json_3.wsim.[REDACTED].tmp/.wing/preflight.js:11 + const Student = require(\\"./Student.Struct.js\\")($stdlib.std.Struct, $stdlib.core.NodeJsCode.fromInline); + const invalidAdvisorInArray = ({\\"name\\": \\"cool\\",\\"age\\": \\"not a number\\",\\"advisors\\": [({\\"id\\": \\"advisor1\\",\\"name\\": \\"Bob\\",\\"age\\": 34}), ({\\"id\\": 10,\\"name\\": \\"Jacob\\",\\"age\\": 45})]}); +>> (Student.fromJson(invalidAdvisorInArray)); + } + } + + + +Tests 1 failed (1) +Test Files 1 failed (1) +Duration " +`; + +exports[`struct_from_json_4.w 1`] = ` +"ERROR: unable to parse Student: + instance.advisors contains duplicate item + +../../../examples/tests/error/target/test/struct_from_json_4.wsim.[REDACTED].tmp/.wing/preflight.js:11 + const Student = require(\\"./Student.Struct.js\\")($stdlib.std.Struct, $stdlib.core.NodeJsCode.fromInline); + const invalidAdvisorInArray = ({\\"name\\": \\"cool\\",\\"age\\": 22,\\"advisors\\": [({\\"id\\": \\"advisor1\\",\\"name\\": \\"Bob\\",\\"age\\": 34}), ({\\"id\\": \\"advisor1\\",\\"name\\": \\"Bob\\",\\"age\\": 34})]}); +>> (Student.fromJson(invalidAdvisorInArray)); + } + } + + + +Tests 1 failed (1) +Test Files 1 failed (1) +Duration " +`; + +exports[`struct_from_json_5.w 1`] = ` +"ERROR: unable to parse Foo: + instance.names.c is not of a type(s) string + +../../../examples/tests/error/target/test/struct_from_json_5.wsim.[REDACTED].tmp/.wing/preflight.js:9 + const Foo = require(\\"./Foo.Struct.js\\")($stdlib.std.Struct, $stdlib.core.NodeJsCode.fromInline); + const jFoo = ({\\"names\\": ({\\"a\\": \\"Amanda\\",\\"b\\": \\"Barry\\",\\"c\\": 10})}); +>> (Foo.fromJson(jFoo)); + } + } + + + Tests 1 failed (1) Test Files 1 failed (1) Duration " diff --git a/tools/hangar/__snapshots__/invalid.ts.snap b/tools/hangar/__snapshots__/invalid.ts.snap index 7134e563639..483a99a00d6 100644 --- a/tools/hangar/__snapshots__/invalid.ts.snap +++ b/tools/hangar/__snapshots__/invalid.ts.snap @@ -2195,6 +2195,35 @@ error: Expected 2 positional argument(s) but got 1 +Tests 1 failed (1) +Test Files 1 failed (1) +Duration " +`; + +exports[`struct_json_conversion.w 1`] = ` +"error: Struct \\"A\\" contains field \\"b\\" which cannot be represented in Json + --> ../../../examples/tests/invalid/struct_json_conversion.w:8:3 + | +8 | A.fromJson({}); + | ^^^^^^^^ Struct \\"A\\" contains field \\"b\\" which cannot be represented in Json + + +error: Struct \\"B\\" contains field \\"a\\" which cannot be represented in Json + --> ../../../examples/tests/invalid/struct_json_conversion.w:15:3 + | +15 | B.fromJson({}); + | ^^^^^^^^ Struct \\"B\\" contains field \\"a\\" which cannot be represented in Json + + +error: Struct \\"C\\" contains field \\"b\\" which cannot be represented in Json + --> ../../../examples/tests/invalid/struct_json_conversion.w:22:3 + | +22 | C.fromJson({}); + | ^^^^^^^^ Struct \\"C\\" contains field \\"b\\" which cannot be represented in Json + + + + Tests 1 failed (1) Test Files 1 failed (1) Duration " diff --git a/tools/hangar/__snapshots__/test_corpus/sdk_tests/bucket/events.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/sdk_tests/bucket/events.w_compile_tf-aws.md index f96db0189ad..4ffe326a213 100644 --- a/tools/hangar/__snapshots__/test_corpus/sdk_tests/bucket/events.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/sdk_tests/bucket/events.w_compile_tf-aws.md @@ -1,5 +1,42 @@ # [events.w](../../../../../../examples/tests/sdk_tests/bucket/events.w) | compile | tf-aws +## CheckHitCountOptions.Struct.js +```js +module.exports = function(stdStruct, fromInline) { + class CheckHitCountOptions { + static jsonSchema() { + return { + id: "/CheckHitCountOptions", + type: "object", + properties: { + key: { type: "string" }, + type: { type: "string" }, + source: { "$ref": "#/$defs/Source" }, + count: { type: "number" }, + }, + required: [ + "key", + "type", + "source", + "count", + ], + $defs: { + "Source": { type: "object", "properties": require("./Source.Struct.js")().jsonSchema().properties }, + } + } + } + static fromJson(obj) { + return stdStruct._validate(obj, this.jsonSchema()) + } + static _toInflightType(context) { + return fromInline(`require("./CheckHitCountOptions.Struct.js")(${ context._lift(stdStruct) })`); + } + } + return CheckHitCountOptions; +}; + +``` + ## inflight.$Closure1-1.js ```js module.exports = function({ $idsCounter, $table }) { @@ -1326,6 +1363,7 @@ class $Root extends $stdlib.std.Resource { (b.onCreate(new $Closure4(this,"$Closure4"))); (b.onEvent(new $Closure5(this,"$Closure5"))); const wait = new $Closure6(this,"$Closure6"); + const CheckHitCountOptions = require("./CheckHitCountOptions.Struct.js")($stdlib.std.Struct, $stdlib.core.NodeJsCode.fromInline); const checkHitCount = new $Closure7(this,"$Closure7"); this.node.root.new("@winglang/sdk.std.Test",std.Test,this,"hitCount is incremented according to the bucket event",new $Closure8(this,"$Closure8"),{ timeout: (std.Duration.fromSeconds(480)) }); } diff --git a/tools/hangar/__snapshots__/test_corpus/valid/bring_local.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/bring_local.w_compile_tf-aws.md index c5ff98955ba..0599af1b4d0 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/bring_local.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/bring_local.w_compile_tf-aws.md @@ -1,5 +1,37 @@ # [bring_local.w](../../../../../examples/tests/valid/bring_local.w) | compile | tf-aws +## Point.Struct.js +```js +module.exports = function(stdStruct, fromInline) { + class Point { + static jsonSchema() { + return { + id: "/Point", + type: "object", + properties: { + x: { type: "number" }, + y: { type: "number" }, + }, + required: [ + "x", + "y", + ], + $defs: { + } + } + } + static fromJson(obj) { + return stdStruct._validate(obj, this.jsonSchema()) + } + static _toInflightType(context) { + return fromInline(`require("./Point.Struct.js")(${ context._lift(stdStruct) })`); + } + } + return Point; +}; + +``` + ## inflight.$Closure1-1.js ```js module.exports = function({ $__parent_this_1_b }) { @@ -568,6 +600,7 @@ module.exports = function({ $stdlib }) { return tmp; })({}) ; + const Point = require("./Point.Struct.js")($stdlib.std.Struct, $stdlib.core.NodeJsCode.fromInline); return { Util, Store, Color }; }; diff --git a/tools/hangar/__snapshots__/test_corpus/valid/deep_equality.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/deep_equality.w_compile_tf-aws.md index 5cc87544aae..f63bd87ec8e 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/deep_equality.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/deep_equality.w_compile_tf-aws.md @@ -1,5 +1,37 @@ # [deep_equality.w](../../../../../examples/tests/valid/deep_equality.w) | compile | tf-aws +## Cat.Struct.js +```js +module.exports = function(stdStruct, fromInline) { + class Cat { + static jsonSchema() { + return { + id: "/Cat", + type: "object", + properties: { + name: { type: "string" }, + age: { type: "number" }, + }, + required: [ + "name", + "age", + ], + $defs: { + } + } + } + static fromJson(obj) { + return stdStruct._validate(obj, this.jsonSchema()) + } + static _toInflightType(context) { + return fromInline(`require("./Cat.Struct.js")(${ context._lift(stdStruct) })`); + } + } + return Cat; +}; + +``` + ## inflight.$Closure1-1.js ```js module.exports = function({ $numA, $numB, $strA, $strB }) { @@ -1513,6 +1545,7 @@ class $Root extends $stdlib.std.Resource { const arrayC = [4, 5, 6]; this.node.root.new("@winglang/sdk.std.Test",std.Test,this,"test:Array with the same value",new $Closure9(this,"$Closure9")); this.node.root.new("@winglang/sdk.std.Test",std.Test,this,"test:Array with different values",new $Closure10(this,"$Closure10")); + const Cat = require("./Cat.Struct.js")($stdlib.std.Struct, $stdlib.core.NodeJsCode.fromInline); const cat1 = ({"name": "Mittens","age": 3}); const cat2 = ({"name": "Mittens","age": 3}); const cat3 = ({"name": "Simba","age": 5}); diff --git a/tools/hangar/__snapshots__/test_corpus/valid/inflight_class_as_struct_members.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/inflight_class_as_struct_members.w_compile_tf-aws.md index a193cd94c73..5f872e30c2b 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/inflight_class_as_struct_members.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/inflight_class_as_struct_members.w_compile_tf-aws.md @@ -1,5 +1,36 @@ # [inflight_class_as_struct_members.w](../../../../../examples/tests/valid/inflight_class_as_struct_members.w) | compile | tf-aws +## Bar.Struct.js +```js +module.exports = function(stdStruct, fromInline) { + class Bar { + static jsonSchema() { + return { + id: "/Bar", + type: "object", + properties: { + foo: { "$ref": "#/$defs/Foo" }, + }, + required: [ + "foo", + ], + $defs: { + "Foo": { type: "object", "properties": require("./Foo.Struct.js")().jsonSchema().properties }, + } + } + } + static fromJson(obj) { + return stdStruct._validate(obj, this.jsonSchema()) + } + static _toInflightType(context) { + return fromInline(`require("./Bar.Struct.js")(${ context._lift(stdStruct) })`); + } + } + return Bar; +}; + +``` + ## inflight.$Closure1-1.js ```js module.exports = function({ $Foo }) { @@ -259,6 +290,7 @@ class $Root extends $stdlib.std.Resource { super._registerBind(host, ops); } } + const Bar = require("./Bar.Struct.js")($stdlib.std.Struct, $stdlib.core.NodeJsCode.fromInline); const getBar = new $Closure1(this,"$Closure1"); this.node.root.new("@winglang/sdk.std.Test",std.Test,this,"test:test",new $Closure2(this,"$Closure2")); } diff --git a/tools/hangar/__snapshots__/test_corpus/valid/optionals.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/optionals.w_compile_tf-aws.md index dea193618be..ad04fccacd1 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/optionals.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/optionals.w_compile_tf-aws.md @@ -1,5 +1,69 @@ # [optionals.w](../../../../../examples/tests/valid/optionals.w) | compile | tf-aws +## Name.Struct.js +```js +module.exports = function(stdStruct, fromInline) { + class Name { + static jsonSchema() { + return { + id: "/Name", + type: "object", + properties: { + first: { type: "string" }, + last: { type: "string" }, + }, + required: [ + "first", + ], + $defs: { + } + } + } + static fromJson(obj) { + return stdStruct._validate(obj, this.jsonSchema()) + } + static _toInflightType(context) { + return fromInline(`require("./Name.Struct.js")(${ context._lift(stdStruct) })`); + } + } + return Name; +}; + +``` + +## Payload.Struct.js +```js +module.exports = function(stdStruct, fromInline) { + class Payload { + static jsonSchema() { + return { + id: "/Payload", + type: "object", + properties: { + a: { type: "string" }, + b: { type: "object", patternProperties: { ".*": { type: "string" } } }, + c: { "$ref": "#/$defs/cloud" }, + }, + required: [ + "a", + ], + $defs: { + "cloud": { type: "object", "properties": require("./cloud.Struct.js")().jsonSchema().properties }, + } + } + } + static fromJson(obj) { + return stdStruct._validate(obj, this.jsonSchema()) + } + static _toInflightType(context) { + return fromInline(`require("./Payload.Struct.js")(${ context._lift(stdStruct) })`); + } + } + return Payload; +}; + +``` + ## inflight.$Closure1-1.js ```js module.exports = function({ $__payloadWithBucket_c_____null_, $__payloadWithoutOptions_b_____null_, $payloadWithBucket_c }) { @@ -390,6 +454,7 @@ class $Root extends $stdlib.std.Resource { const optionalSup = new Super(this,"Super"); const s = (optionalSup ?? new Sub(this,"Sub")); {((cond) => {if (!cond) throw new Error("assertion failed: s.name == \"Super\"")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(s.name,"Super")))}; + const Name = require("./Name.Struct.js")($stdlib.std.Struct, $stdlib.core.NodeJsCode.fromInline); let name = ({"first": "John","last": "Doe"}); { const $IF_LET_VALUE = name; @@ -503,6 +568,7 @@ class $Root extends $stdlib.std.Resource { {((cond) => {if (!cond) throw new Error("assertion failed: o.value == 1")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(o.value,1)))}; } } + const Payload = require("./Payload.Struct.js")($stdlib.std.Struct, $stdlib.core.NodeJsCode.fromInline); const payloadWithoutOptions = ({"a": "a"}); const payloadWithBucket = ({"a": "a","c": this.node.root.newAbstract("@winglang/sdk.cloud.Bucket",this,"orange bucket")}); this.node.root.new("@winglang/sdk.std.Test",std.Test,this,"test:t",new $Closure1(this,"$Closure1")); diff --git a/tools/hangar/__snapshots__/test_corpus/valid/store.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/store.w_compile_tf-aws.md index a1383478d1a..7e637f6ef1b 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/store.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/store.w_compile_tf-aws.md @@ -1,5 +1,37 @@ # [store.w](../../../../../examples/tests/valid/store.w) | compile | tf-aws +## Point.Struct.js +```js +module.exports = function(stdStruct, fromInline) { + class Point { + static jsonSchema() { + return { + id: "/Point", + type: "object", + properties: { + x: { type: "number" }, + y: { type: "number" }, + }, + required: [ + "x", + "y", + ], + $defs: { + } + } + } + static fromJson(obj) { + return stdStruct._validate(obj, this.jsonSchema()) + } + static _toInflightType(context) { + return fromInline(`require("./Point.Struct.js")(${ context._lift(stdStruct) })`); + } + } + return Point; +}; + +``` + ## inflight.$Closure1-1.js ```js module.exports = function({ $__parent_this_1_b }) { @@ -197,6 +229,7 @@ class $Root extends $stdlib.std.Resource { return tmp; })({}) ; + const Point = require("./Point.Struct.js")($stdlib.std.Struct, $stdlib.core.NodeJsCode.fromInline); } } const $App = $stdlib.core.App.for(process.env.WING_TARGET); diff --git a/tools/hangar/__snapshots__/test_corpus/valid/struct_from_json.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/struct_from_json.w_compile_tf-aws.md new file mode 100644 index 00000000000..1b4f392312e --- /dev/null +++ b/tools/hangar/__snapshots__/test_corpus/valid/struct_from_json.w_compile_tf-aws.md @@ -0,0 +1,740 @@ +# [struct_from_json.w](../../../../../examples/tests/valid/struct_from_json.w) | compile | tf-aws + +## Advisor.Struct.js +```js +module.exports = function(stdStruct, fromInline) { + class Advisor { + static jsonSchema() { + return { + id: "/Advisor", + type: "object", + properties: { + ...require("./Person.Struct.js")().jsonSchema().properties, + employeeID: { type: "string" }, + }, + required: [ + "employeeID", + ...require("./Person.Struct.js")().jsonSchema().required, + ], + $defs: { + ...require("./Person.Struct.js")().jsonSchema().$defs, + } + } + } + static fromJson(obj) { + return stdStruct._validate(obj, this.jsonSchema()) + } + static _toInflightType(context) { + return fromInline(`require("./Advisor.Struct.js")(${ context._lift(stdStruct) })`); + } + } + return Advisor; +}; + +``` + +## Bar.Struct.js +```js +module.exports = function(stdStruct, fromInline) { + class Bar { + static jsonSchema() { + return { + id: "/Bar", + type: "object", + properties: { + ...require("./Foo.Struct.js")().jsonSchema().properties, + b: { type: "number" }, + }, + required: [ + "b", + ...require("./Foo.Struct.js")().jsonSchema().required, + ], + $defs: { + ...require("./Foo.Struct.js")().jsonSchema().$defs, + } + } + } + static fromJson(obj) { + return stdStruct._validate(obj, this.jsonSchema()) + } + static _toInflightType(context) { + return fromInline(`require("./Bar.Struct.js")(${ context._lift(stdStruct) })`); + } + } + return Bar; +}; + +``` + +## Course.Struct.js +```js +module.exports = function(stdStruct, fromInline) { + class Course { + static jsonSchema() { + return { + id: "/Course", + type: "object", + properties: { + name: { type: "string" }, + credits: { type: "number" }, + }, + required: [ + "name", + "credits", + ], + $defs: { + } + } + } + static fromJson(obj) { + return stdStruct._validate(obj, this.jsonSchema()) + } + static _toInflightType(context) { + return fromInline(`require("./Course.Struct.js")(${ context._lift(stdStruct) })`); + } + } + return Course; +}; + +``` + +## CourseResults.Struct.js +```js +module.exports = function(stdStruct, fromInline) { + class CourseResults { + static jsonSchema() { + return { + id: "/CourseResults", + type: "object", + properties: { + course: { "$ref": "#/$defs/Course" }, + grade: { type: "string" }, + dateTaken: { "$ref": "#/$defs/Date" }, + }, + required: [ + "course", + "grade", + "dateTaken", + ], + $defs: { + "Course": { type: "object", "properties": require("./Course.Struct.js")().jsonSchema().properties }, + "Date": { type: "object", "properties": require("./Date.Struct.js")().jsonSchema().properties }, + } + } + } + static fromJson(obj) { + return stdStruct._validate(obj, this.jsonSchema()) + } + static _toInflightType(context) { + return fromInline(`require("./CourseResults.Struct.js")(${ context._lift(stdStruct) })`); + } + } + return CourseResults; +}; + +``` + +## Date.Struct.js +```js +module.exports = function(stdStruct, fromInline) { + class Date { + static jsonSchema() { + return { + id: "/Date", + type: "object", + properties: { + month: { type: "number" }, + day: { type: "number" }, + year: { type: "number" }, + }, + required: [ + "month", + "day", + "year", + ], + $defs: { + } + } + } + static fromJson(obj) { + return stdStruct._validate(obj, this.jsonSchema()) + } + static _toInflightType(context) { + return fromInline(`require("./Date.Struct.js")(${ context._lift(stdStruct) })`); + } + } + return Date; +}; + +``` + +## Foo.Struct.js +```js +module.exports = function(stdStruct, fromInline) { + class Foo { + static jsonSchema() { + return { + id: "/Foo", + type: "object", + properties: { + f: { type: "string" }, + }, + required: [ + "f", + ], + $defs: { + } + } + } + static fromJson(obj) { + return stdStruct._validate(obj, this.jsonSchema()) + } + static _toInflightType(context) { + return fromInline(`require("./Foo.Struct.js")(${ context._lift(stdStruct) })`); + } + } + return Foo; +}; + +``` + +## Foosible.Struct.js +```js +module.exports = function(stdStruct, fromInline) { + class Foosible { + static jsonSchema() { + return { + id: "/Foosible", + type: "object", + properties: { + f: { type: "string" }, + }, + required: [ + ], + $defs: { + } + } + } + static fromJson(obj) { + return stdStruct._validate(obj, this.jsonSchema()) + } + static _toInflightType(context) { + return fromInline(`require("./Foosible.Struct.js")(${ context._lift(stdStruct) })`); + } + } + return Foosible; +}; + +``` + +## Person.Struct.js +```js +module.exports = function(stdStruct, fromInline) { + class Person { + static jsonSchema() { + return { + id: "/Person", + type: "object", + properties: { + firstName: { type: "string" }, + lastName: { type: "string" }, + dob: { "$ref": "#/$defs/Date" }, + }, + required: [ + "firstName", + "lastName", + "dob", + ], + $defs: { + "Date": { type: "object", "properties": require("./Date.Struct.js")().jsonSchema().properties }, + } + } + } + static fromJson(obj) { + return stdStruct._validate(obj, this.jsonSchema()) + } + static _toInflightType(context) { + return fromInline(`require("./Person.Struct.js")(${ context._lift(stdStruct) })`); + } + } + return Person; +}; + +``` + +## Student.Struct.js +```js +module.exports = function(stdStruct, fromInline) { + class Student { + static jsonSchema() { + return { + id: "/Student", + type: "object", + properties: { + ...require("./Person.Struct.js")().jsonSchema().properties, + enrolled: { type: "boolean" }, + schoolId: { type: "string" }, + advisor: { "$ref": "#/$defs/Advisor" }, + enrolledCourses: { type: "array", uniqueItems: true, items: { "$ref": "#/$defs/Course" } }, + coursesTaken: { type: "array", items: { "$ref": "#/$defs/CourseResults" } }, + additionalData: { type: "object" }, + }, + required: [ + "enrolled", + "schoolId", + ...require("./Person.Struct.js")().jsonSchema().required, + ], + $defs: { + "Advisor": { type: "object", "properties": require("./Advisor.Struct.js")().jsonSchema().properties }, + "Course": { type: "object", "properties": require("./Course.Struct.js")().jsonSchema().properties }, + "CourseResults": { type: "object", "properties": require("./CourseResults.Struct.js")().jsonSchema().properties }, + ...require("./Person.Struct.js")().jsonSchema().$defs, + } + } + } + static fromJson(obj) { + return stdStruct._validate(obj, this.jsonSchema()) + } + static _toInflightType(context) { + return fromInline(`require("./Student.Struct.js")(${ context._lift(stdStruct) })`); + } + } + return Student; +}; + +``` + +## inflight.$Closure1-1.js +```js +module.exports = function({ $Student }) { + class $Closure1 { + constructor({ }) { + const $obj = (...args) => this.handle(...args); + Object.setPrototypeOf($obj, this); + return $obj; + } + async handle() { + const jStudent3 = ({"firstName": "struct","lastName": "greatest","enrolled": true,"schoolId": "s3-inflight","dob": ({"month": 4,"day": 1,"year": 1999}),"coursesTaken": [({"grade": "B","dateTaken": ({"month": 5,"day": 10,"year": 2021}),"course": ({"name": "COMP 101","credits": 2})}), ({"grade": "A","dateTaken": ({"month": 5,"day": 10,"year": 2021}),"course": ({"name": "COMP 121","credits": 4})})]}); + const studentInflight1 = ($Student.fromJson(jStudent3)); + {((cond) => {if (!cond) throw new Error("assertion failed: studentInflight1.firstName == \"struct\"")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(studentInflight1.firstName,"struct")))}; + {((cond) => {if (!cond) throw new Error("assertion failed: studentInflight1.lastName == \"greatest\"")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(studentInflight1.lastName,"greatest")))}; + {((cond) => {if (!cond) throw new Error("assertion failed: studentInflight1.enrolled")})(studentInflight1.enrolled)}; + {((cond) => {if (!cond) throw new Error("assertion failed: studentInflight1.schoolId == \"s3-inflight\"")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(studentInflight1.schoolId,"s3-inflight")))}; + {((cond) => {if (!cond) throw new Error("assertion failed: studentInflight1.dob.month == 4")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(studentInflight1.dob.month,4)))}; + {((cond) => {if (!cond) throw new Error("assertion failed: studentInflight1.dob.day == 1")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(studentInflight1.dob.day,1)))}; + {((cond) => {if (!cond) throw new Error("assertion failed: studentInflight1.dob.year == 1999")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(studentInflight1.dob.year,1999)))}; + { + const $IF_LET_VALUE = studentInflight1.coursesTaken; + if ($IF_LET_VALUE != undefined) { + const coursesTaken = $IF_LET_VALUE; + const course1 = (await coursesTaken.at(0)); + const course2 = (await coursesTaken.at(1)); + {((cond) => {if (!cond) throw new Error("assertion failed: course1.grade == \"B\"")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(course1.grade,"B")))}; + {((cond) => {if (!cond) throw new Error("assertion failed: course2.grade == \"A\"")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(course2.grade,"A")))}; + } + else { + {((cond) => {if (!cond) throw new Error("assertion failed: false")})(false)}; + } + } + } + } + return $Closure1; +} + +``` + +## inflight.$Closure2-1.js +```js +module.exports = function({ $Student, $jStudent1 }) { + class $Closure2 { + constructor({ }) { + const $obj = (...args) => this.handle(...args); + Object.setPrototypeOf($obj, this); + return $obj; + } + async handle() { + const studentInflight1 = ($Student.fromJson($jStudent1)); + {((cond) => {if (!cond) throw new Error("assertion failed: studentInflight1.firstName == \"John\"")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(studentInflight1.firstName,"John")))}; + {((cond) => {if (!cond) throw new Error("assertion failed: studentInflight1.lastName == \"Smith\"")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(studentInflight1.lastName,"Smith")))}; + {((cond) => {if (!cond) throw new Error("assertion failed: studentInflight1.enrolled")})(studentInflight1.enrolled)}; + {((cond) => {if (!cond) throw new Error("assertion failed: studentInflight1.schoolId == \"s1-xyz\"")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(studentInflight1.schoolId,"s1-xyz")))}; + {((cond) => {if (!cond) throw new Error("assertion failed: studentInflight1.dob.month == 10")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(studentInflight1.dob.month,10)))}; + {((cond) => {if (!cond) throw new Error("assertion failed: studentInflight1.dob.day == 10")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(studentInflight1.dob.day,10)))}; + {((cond) => {if (!cond) throw new Error("assertion failed: studentInflight1.dob.year == 2005")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(studentInflight1.dob.year,2005)))}; + } + } + return $Closure2; +} + +``` + +## main.tf.json +```json +{ + "//": { + "metadata": { + "backend": "local", + "stackName": "root", + "version": "0.17.0" + }, + "outputs": { + "root": { + "Default": { + "cloud.TestRunner": { + "TestFunctionArns": "WING_TEST_RUNNER_FUNCTION_ARNS" + } + } + } + } + }, + "output": { + "WING_TEST_RUNNER_FUNCTION_ARNS": { + "value": "[[\"root/Default/Default/test:flight school student :)\",\"${aws_lambda_function.testflightschoolstudent_Handler_8BE7AA78.arn}\"],[\"root/Default/Default/test:lifting a student\",\"${aws_lambda_function.testliftingastudent_Handler_30A43B55.arn}\"]]" + } + }, + "provider": { + "aws": [ + {} + ] + }, + "resource": { + "aws_iam_role": { + "testflightschoolstudent_Handler_IamRole_5F1C920A": { + "//": { + "metadata": { + "path": "root/Default/Default/test:flight school student :)/Handler/IamRole", + "uniqueId": "testflightschoolstudent_Handler_IamRole_5F1C920A" + } + }, + "assume_role_policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":\"sts:AssumeRole\",\"Principal\":{\"Service\":\"lambda.amazonaws.com\"},\"Effect\":\"Allow\"}]}" + }, + "testliftingastudent_Handler_IamRole_66279A05": { + "//": { + "metadata": { + "path": "root/Default/Default/test:lifting a student/Handler/IamRole", + "uniqueId": "testliftingastudent_Handler_IamRole_66279A05" + } + }, + "assume_role_policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":\"sts:AssumeRole\",\"Principal\":{\"Service\":\"lambda.amazonaws.com\"},\"Effect\":\"Allow\"}]}" + } + }, + "aws_iam_role_policy": { + "testflightschoolstudent_Handler_IamRolePolicy_942EA77E": { + "//": { + "metadata": { + "path": "root/Default/Default/test:flight school student :)/Handler/IamRolePolicy", + "uniqueId": "testflightschoolstudent_Handler_IamRolePolicy_942EA77E" + } + }, + "policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":\"none:null\",\"Resource\":\"*\"}]}", + "role": "${aws_iam_role.testflightschoolstudent_Handler_IamRole_5F1C920A.name}" + }, + "testliftingastudent_Handler_IamRolePolicy_13D30A25": { + "//": { + "metadata": { + "path": "root/Default/Default/test:lifting a student/Handler/IamRolePolicy", + "uniqueId": "testliftingastudent_Handler_IamRolePolicy_13D30A25" + } + }, + "policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":\"none:null\",\"Resource\":\"*\"}]}", + "role": "${aws_iam_role.testliftingastudent_Handler_IamRole_66279A05.name}" + } + }, + "aws_iam_role_policy_attachment": { + "testflightschoolstudent_Handler_IamRolePolicyAttachment_57666E60": { + "//": { + "metadata": { + "path": "root/Default/Default/test:flight school student :)/Handler/IamRolePolicyAttachment", + "uniqueId": "testflightschoolstudent_Handler_IamRolePolicyAttachment_57666E60" + } + }, + "policy_arn": "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + "role": "${aws_iam_role.testflightschoolstudent_Handler_IamRole_5F1C920A.name}" + }, + "testliftingastudent_Handler_IamRolePolicyAttachment_4843B297": { + "//": { + "metadata": { + "path": "root/Default/Default/test:lifting a student/Handler/IamRolePolicyAttachment", + "uniqueId": "testliftingastudent_Handler_IamRolePolicyAttachment_4843B297" + } + }, + "policy_arn": "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + "role": "${aws_iam_role.testliftingastudent_Handler_IamRole_66279A05.name}" + } + }, + "aws_lambda_function": { + "testflightschoolstudent_Handler_8BE7AA78": { + "//": { + "metadata": { + "path": "root/Default/Default/test:flight school student :)/Handler/Default", + "uniqueId": "testflightschoolstudent_Handler_8BE7AA78" + } + }, + "environment": { + "variables": { + "WING_FUNCTION_NAME": "Handler-c85c011b", + "WING_TARGET": "tf-aws" + } + }, + "function_name": "Handler-c85c011b", + "handler": "index.handler", + "publish": true, + "role": "${aws_iam_role.testflightschoolstudent_Handler_IamRole_5F1C920A.arn}", + "runtime": "nodejs18.x", + "s3_bucket": "${aws_s3_bucket.Code.bucket}", + "s3_key": "${aws_s3_object.testflightschoolstudent_Handler_S3Object_1D59FBCC.key}", + "timeout": 30, + "vpc_config": { + "security_group_ids": [], + "subnet_ids": [] + } + }, + "testliftingastudent_Handler_30A43B55": { + "//": { + "metadata": { + "path": "root/Default/Default/test:lifting a student/Handler/Default", + "uniqueId": "testliftingastudent_Handler_30A43B55" + } + }, + "environment": { + "variables": { + "WING_FUNCTION_NAME": "Handler-c82f8661", + "WING_TARGET": "tf-aws" + } + }, + "function_name": "Handler-c82f8661", + "handler": "index.handler", + "publish": true, + "role": "${aws_iam_role.testliftingastudent_Handler_IamRole_66279A05.arn}", + "runtime": "nodejs18.x", + "s3_bucket": "${aws_s3_bucket.Code.bucket}", + "s3_key": "${aws_s3_object.testliftingastudent_Handler_S3Object_51C773C7.key}", + "timeout": 30, + "vpc_config": { + "security_group_ids": [], + "subnet_ids": [] + } + } + }, + "aws_s3_bucket": { + "Code": { + "//": { + "metadata": { + "path": "root/Default/Code", + "uniqueId": "Code" + } + }, + "bucket_prefix": "code-c84a50b1-" + } + }, + "aws_s3_object": { + "testflightschoolstudent_Handler_S3Object_1D59FBCC": { + "//": { + "metadata": { + "path": "root/Default/Default/test:flight school student :)/Handler/S3Object", + "uniqueId": "testflightschoolstudent_Handler_S3Object_1D59FBCC" + } + }, + "bucket": "${aws_s3_bucket.Code.bucket}", + "key": "", + "source": "" + }, + "testliftingastudent_Handler_S3Object_51C773C7": { + "//": { + "metadata": { + "path": "root/Default/Default/test:lifting a student/Handler/S3Object", + "uniqueId": "testliftingastudent_Handler_S3Object_51C773C7" + } + }, + "bucket": "${aws_s3_bucket.Code.bucket}", + "key": "", + "source": "" + } + } + } +} +``` + +## preflight.js +```js +const $stdlib = require('@winglang/sdk'); +const $outdir = process.env.WING_SYNTH_DIR ?? "."; +const $wing_is_test = process.env.WING_IS_TEST === "true"; +const std = $stdlib.std; +class $Root extends $stdlib.std.Resource { + constructor(scope, id) { + super(scope, id); + class $Closure1 extends $stdlib.std.Resource { + constructor(scope, id, ) { + super(scope, id); + this._addInflightOps("handle", "$inflight_init"); + this.display.hidden = true; + } + static _toInflightType(context) { + return $stdlib.core.NodeJsCode.fromInline(` + require("./inflight.$Closure1-1.js")({ + $Student: ${context._lift(Student)}, + }) + `); + } + _toInflight() { + return $stdlib.core.NodeJsCode.fromInline(` + (await (async () => { + const $Closure1Client = ${$Closure1._toInflightType(this).text}; + const client = new $Closure1Client({ + }); + if (client.$inflight_init) { await client.$inflight_init(); } + return client; + })()) + `); + } + } + class $Closure2 extends $stdlib.std.Resource { + constructor(scope, id, ) { + super(scope, id); + this._addInflightOps("handle", "$inflight_init"); + this.display.hidden = true; + } + static _toInflightType(context) { + return $stdlib.core.NodeJsCode.fromInline(` + require("./inflight.$Closure2-1.js")({ + $Student: ${context._lift(Student)}, + $jStudent1: ${context._lift(jStudent1)}, + }) + `); + } + _toInflight() { + return $stdlib.core.NodeJsCode.fromInline(` + (await (async () => { + const $Closure2Client = ${$Closure2._toInflightType(this).text}; + const client = new $Closure2Client({ + }); + if (client.$inflight_init) { await client.$inflight_init(); } + return client; + })()) + `); + } + _registerBind(host, ops) { + if (ops.includes("handle")) { + $Closure2._registerBindObject(jStudent1, host, []); + } + super._registerBind(host, ops); + } + } + const Foo = require("./Foo.Struct.js")($stdlib.std.Struct, $stdlib.core.NodeJsCode.fromInline); + const jFoo = ({"f": "bar"}); + {((cond) => {if (!cond) throw new Error("assertion failed: Foo.fromJson(jFoo).f == \"bar\"")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })((Foo.fromJson(jFoo)).f,"bar")))}; + const Foosible = require("./Foosible.Struct.js")($stdlib.std.Struct, $stdlib.core.NodeJsCode.fromInline); + const jFoosible = ({}); + const jFoosible2 = ({"f": "bar"}); + { + const $IF_LET_VALUE = (Foosible.fromJson(jFoosible)).f; + if ($IF_LET_VALUE != undefined) { + const f = $IF_LET_VALUE; + {((cond) => {if (!cond) throw new Error("assertion failed: false")})(false)}; + } + } + { + const $IF_LET_VALUE = (Foosible.fromJson(jFoosible2)).f; + if ($IF_LET_VALUE != undefined) { + const f = $IF_LET_VALUE; + {((cond) => {if (!cond) throw new Error("assertion failed: f == \"bar\"")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(f,"bar")))}; + } + else { + {((cond) => {if (!cond) throw new Error("assertion failed: false")})(false)}; + } + } + const Bar = require("./Bar.Struct.js")($stdlib.std.Struct, $stdlib.core.NodeJsCode.fromInline); + const jBar = ({"f": "bar","b": 10}); + const b = (Bar.fromJson(jBar)); + {((cond) => {if (!cond) throw new Error("assertion failed: b.f == \"bar\"")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(b.f,"bar")))}; + {((cond) => {if (!cond) throw new Error("assertion failed: b.b == 10")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(b.b,10)))}; + const Date = require("./Date.Struct.js")($stdlib.std.Struct, $stdlib.core.NodeJsCode.fromInline); + const Person = require("./Person.Struct.js")($stdlib.std.Struct, $stdlib.core.NodeJsCode.fromInline); + const Advisor = require("./Advisor.Struct.js")($stdlib.std.Struct, $stdlib.core.NodeJsCode.fromInline); + const Course = require("./Course.Struct.js")($stdlib.std.Struct, $stdlib.core.NodeJsCode.fromInline); + const CourseResults = require("./CourseResults.Struct.js")($stdlib.std.Struct, $stdlib.core.NodeJsCode.fromInline); + const Student = require("./Student.Struct.js")($stdlib.std.Struct, $stdlib.core.NodeJsCode.fromInline); + const jStudent1 = ({"firstName": "John","lastName": "Smith","enrolled": true,"schoolId": "s1-xyz","dob": ({"month": 10,"day": 10,"year": 2005}),"enrolledCourses": []}); + const student1 = (Student.fromJson(jStudent1)); + {((cond) => {if (!cond) throw new Error("assertion failed: student1.firstName == \"John\"")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(student1.firstName,"John")))}; + {((cond) => {if (!cond) throw new Error("assertion failed: student1.lastName == \"Smith\"")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(student1.lastName,"Smith")))}; + {((cond) => {if (!cond) throw new Error("assertion failed: student1.enrolled")})(student1.enrolled)}; + {((cond) => {if (!cond) throw new Error("assertion failed: student1.schoolId == \"s1-xyz\"")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(student1.schoolId,"s1-xyz")))}; + {((cond) => {if (!cond) throw new Error("assertion failed: student1.dob.month == 10")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(student1.dob.month,10)))}; + {((cond) => {if (!cond) throw new Error("assertion failed: student1.dob.day == 10")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(student1.dob.day,10)))}; + {((cond) => {if (!cond) throw new Error("assertion failed: student1.dob.year == 2005")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(student1.dob.year,2005)))}; + const jStudent2 = ({"advisor": ({"firstName": "Tom","lastName": "Baker","dob": ({"month": 1,"day": 1,"year": 1983}),"employeeID": "emp123"}),"firstName": "Sally","lastName": "Reynolds","enrolled": false,"schoolId": "s2-xyz","dob": ({"month": 5,"day": 31,"year": 1987}),"enrolledCourses": [({"name": "COMP 101","credits": 2}), ({"name": "COMP 121","credits": 4})],"coursesTaken": [({"grade": "F","dateTaken": ({"month": 5,"day": 10,"year": 2021}),"course": ({"name": "COMP 101","credits": 2})}), ({"grade": "D","dateTaken": ({"month": 5,"day": 10,"year": 2021}),"course": ({"name": "COMP 121","credits": 4})})]}); + const student2 = (Student.fromJson(jStudent2)); + {((cond) => {if (!cond) throw new Error("assertion failed: student2.firstName == \"Sally\"")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(student2.firstName,"Sally")))}; + {((cond) => {if (!cond) throw new Error("assertion failed: student2.lastName == \"Reynolds\"")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(student2.lastName,"Reynolds")))}; + {((cond) => {if (!cond) throw new Error("assertion failed: !student2.enrolled")})((!student2.enrolled))}; + {((cond) => {if (!cond) throw new Error("assertion failed: student2.schoolId == \"s2-xyz\"")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(student2.schoolId,"s2-xyz")))}; + {((cond) => {if (!cond) throw new Error("assertion failed: student2.dob.month == 5")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(student2.dob.month,5)))}; + {((cond) => {if (!cond) throw new Error("assertion failed: student2.dob.day == 31")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(student2.dob.day,31)))}; + {((cond) => {if (!cond) throw new Error("assertion failed: student2.dob.year == 1987")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(student2.dob.year,1987)))}; + { + const $IF_LET_VALUE = student2.enrolledCourses; + if ($IF_LET_VALUE != undefined) { + const enrolledCourses = $IF_LET_VALUE; + const courses = [...(enrolledCourses)]; + const s2Course1 = (courses.at(0)); + const s2Course2 = (courses.at(1)); + {((cond) => {if (!cond) throw new Error("assertion failed: s2Course1.name == \"COMP 101\"")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(s2Course1.name,"COMP 101")))}; + {((cond) => {if (!cond) throw new Error("assertion failed: s2Course1.credits == 2")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(s2Course1.credits,2)))}; + {((cond) => {if (!cond) throw new Error("assertion failed: s2Course2.name == \"COMP 121\"")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(s2Course2.name,"COMP 121")))}; + {((cond) => {if (!cond) throw new Error("assertion failed: s2Course2.credits == 4")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(s2Course2.credits,4)))}; + } + else { + {((cond) => {if (!cond) throw new Error("assertion failed: false")})(false)}; + } + } + const jStudent3 = ({"enrolled": false,"schoolId": "w/e","firstName": student2.firstName,"lastName": student2.lastName,"dob": ({"month": 1,"day": 1,"year": 1959}),"additionalData": ({"notes": "wow such notes","legacy": false,"emergencyContactsNumbers": ["123-345-9928"]})}); + const student3 = (Student.fromJson(jStudent3)); + { + const $IF_LET_VALUE = student3.additionalData; + if ($IF_LET_VALUE != undefined) { + const additionalData = $IF_LET_VALUE; + const notes = (additionalData)["notes"]; + {((cond) => {if (!cond) throw new Error("assertion failed: notes == \"wow such notes\"")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(notes,"wow such notes")))}; + } + else { + {((cond) => {if (!cond) throw new Error("assertion failed: false")})(false)}; + } + } + const invalidStudent = ({"firstName": "I dont have","lastName": "Any other info"}); + { + const $IF_LET_VALUE = (() => { try { return Student.fromJson(invalidStudent); } catch { return undefined; }})();; + if ($IF_LET_VALUE != undefined) { + const student = $IF_LET_VALUE; + {((cond) => {if (!cond) throw new Error("assertion failed: false")})(false)}; + } + else { + {((cond) => {if (!cond) throw new Error("assertion failed: true")})(true)}; + } + } + { + const $IF_LET_VALUE = (() => { try { return Student.fromJson(jStudent2); } catch { return undefined; }})();; + if ($IF_LET_VALUE != undefined) { + const student = $IF_LET_VALUE; + {((cond) => {if (!cond) throw new Error("assertion failed: student.firstName == \"Sally\"")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(student.firstName,"Sally")))}; + {((cond) => {if (!cond) throw new Error("assertion failed: student.lastName == \"Reynolds\"")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(student.lastName,"Reynolds")))}; + {((cond) => {if (!cond) throw new Error("assertion failed: !student.enrolled")})((!student.enrolled))}; + {((cond) => {if (!cond) throw new Error("assertion failed: student.schoolId == \"s2-xyz\"")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(student.schoolId,"s2-xyz")))}; + {((cond) => {if (!cond) throw new Error("assertion failed: student.dob.month == 5")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(student.dob.month,5)))}; + {((cond) => {if (!cond) throw new Error("assertion failed: student.dob.day == 31")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(student.dob.day,31)))}; + {((cond) => {if (!cond) throw new Error("assertion failed: student.dob.year == 1987")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(student.dob.year,1987)))}; + } + else { + {((cond) => {if (!cond) throw new Error("assertion failed: false")})(false)}; + } + } + this.node.root.new("@winglang/sdk.std.Test",std.Test,this,"test:flight school student :)",new $Closure1(this,"$Closure1")); + this.node.root.new("@winglang/sdk.std.Test",std.Test,this,"test:lifting a student",new $Closure2(this,"$Closure2")); + } +} +const $App = $stdlib.core.App.for(process.env.WING_TARGET); +new $App({ outdir: $outdir, name: "struct_from_json", rootConstruct: $Root, plugins: $plugins, isTestEnvironment: $wing_is_test, entrypointDir: process.env['WING_SOURCE_DIR'], rootId: process.env['WING_ROOT_ID'] }).synth(); + +``` + diff --git a/tools/hangar/__snapshots__/test_corpus/valid/struct_from_json.w_test_sim.md b/tools/hangar/__snapshots__/test_corpus/valid/struct_from_json.w_test_sim.md new file mode 100644 index 00000000000..2960b1ce7d3 --- /dev/null +++ b/tools/hangar/__snapshots__/test_corpus/valid/struct_from_json.w_test_sim.md @@ -0,0 +1,13 @@ +# [struct_from_json.w](../../../../../examples/tests/valid/struct_from_json.w) | test | sim + +## stdout.log +```log +pass ─ struct_from_json.wsim » root/env0/test:flight school student :) +pass ─ struct_from_json.wsim » root/env1/test:lifting a student + + +Tests 2 passed (2) +Test Files 1 passed (1) +Duration +``` + diff --git a/tools/hangar/__snapshots__/test_corpus/valid/structs.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/structs.w_compile_tf-aws.md index 9d89d5ea793..e61ce8a61a3 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/structs.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/structs.w_compile_tf-aws.md @@ -1,5 +1,167 @@ # [structs.w](../../../../../examples/tests/valid/structs.w) | compile | tf-aws +## A.Struct.js +```js +module.exports = function(stdStruct, fromInline) { + class A { + static jsonSchema() { + return { + id: "/A", + type: "object", + properties: { + field0: { type: "string" }, + }, + required: [ + "field0", + ], + $defs: { + } + } + } + static fromJson(obj) { + return stdStruct._validate(obj, this.jsonSchema()) + } + static _toInflightType(context) { + return fromInline(`require("./A.Struct.js")(${ context._lift(stdStruct) })`); + } + } + return A; +}; + +``` + +## B.Struct.js +```js +module.exports = function(stdStruct, fromInline) { + class B { + static jsonSchema() { + return { + id: "/B", + type: "object", + properties: { + ...require("./A.Struct.js")().jsonSchema().properties, + field1: { type: "number" }, + field2: { type: "string" }, + field3: { "$ref": "#/$defs/A" }, + }, + required: [ + "field1", + "field2", + "field3", + ...require("./A.Struct.js")().jsonSchema().required, + ], + $defs: { + "A": { type: "object", "properties": require("./A.Struct.js")().jsonSchema().properties }, + ...require("./A.Struct.js")().jsonSchema().$defs, + } + } + } + static fromJson(obj) { + return stdStruct._validate(obj, this.jsonSchema()) + } + static _toInflightType(context) { + return fromInline(`require("./B.Struct.js")(${ context._lift(stdStruct) })`); + } + } + return B; +}; + +``` + +## Dazzle.Struct.js +```js +module.exports = function(stdStruct, fromInline) { + class Dazzle { + static jsonSchema() { + return { + id: "/Dazzle", + type: "object", + properties: { + a: { type: "string" }, + }, + required: [ + "a", + ], + $defs: { + } + } + } + static fromJson(obj) { + return stdStruct._validate(obj, this.jsonSchema()) + } + static _toInflightType(context) { + return fromInline(`require("./Dazzle.Struct.js")(${ context._lift(stdStruct) })`); + } + } + return Dazzle; +}; + +``` + +## Razzle.Struct.js +```js +module.exports = function(stdStruct, fromInline) { + class Razzle { + static jsonSchema() { + return { + id: "/Razzle", + type: "object", + properties: { + a: { type: "string" }, + }, + required: [ + "a", + ], + $defs: { + } + } + } + static fromJson(obj) { + return stdStruct._validate(obj, this.jsonSchema()) + } + static _toInflightType(context) { + return fromInline(`require("./Razzle.Struct.js")(${ context._lift(stdStruct) })`); + } + } + return Razzle; +}; + +``` + +## Showtime.Struct.js +```js +module.exports = function(stdStruct, fromInline) { + class Showtime { + static jsonSchema() { + return { + id: "/Showtime", + type: "object", + properties: { + ...require("./Razzle.Struct.js")().jsonSchema().properties, + ...require("./Dazzle.Struct.js")().jsonSchema().properties, + }, + required: [ + ...require("./Razzle.Struct.js")().jsonSchema().required, + ...require("./Dazzle.Struct.js")().jsonSchema().required, + ], + $defs: { + ...require("./Razzle.Struct.js")().jsonSchema().$defs, + ...require("./Dazzle.Struct.js")().jsonSchema().$defs, + } + } + } + static fromJson(obj) { + return stdStruct._validate(obj, this.jsonSchema()) + } + static _toInflightType(context) { + return fromInline(`require("./Showtime.Struct.js")(${ context._lift(stdStruct) })`); + } + } + return Showtime; +}; + +``` + ## inflight.$Closure1-1.js ```js module.exports = function({ }) { @@ -35,6 +197,49 @@ module.exports = function({ }) { ``` +## lotsOfTypes.Struct.js +```js +module.exports = function(stdStruct, fromInline) { + class lotsOfTypes { + static jsonSchema() { + return { + id: "/lotsOfTypes", + type: "object", + properties: { + a: { type: "string" }, + b: { type: "number" }, + c: { type: "array", items: { type: "string" } }, + d: { type: "object", patternProperties: { ".*": { type: "string" } } }, + e: { type: "object" }, + f: { type: "boolean" }, + g: { type: "string" }, + h: { type: "array", items: { type: "object", patternProperties: { ".*": { type: "number" } } } }, + }, + required: [ + "a", + "b", + "c", + "d", + "e", + "f", + "h", + ], + $defs: { + } + } + } + static fromJson(obj) { + return stdStruct._validate(obj, this.jsonSchema()) + } + static _toInflightType(context) { + return fromInline(`require("./lotsOfTypes.Struct.js")(${ context._lift(stdStruct) })`); + } + } + return lotsOfTypes; +}; + +``` + ## main.tf.json ```json { @@ -223,11 +428,17 @@ class $Root extends $stdlib.std.Resource { `); } } + const A = require("./A.Struct.js")($stdlib.std.Struct, $stdlib.core.NodeJsCode.fromInline); + const B = require("./B.Struct.js")($stdlib.std.Struct, $stdlib.core.NodeJsCode.fromInline); const x = ({"field0": "Sup"}); const y = ({"field0": "hello","field1": 1,"field2": "world","field3": ({"field0": "foo"})}); {((cond) => {if (!cond) throw new Error("assertion failed: x.field0 == \"Sup\"")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(x.field0,"Sup")))}; {((cond) => {if (!cond) throw new Error("assertion failed: y.field1 == 1")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(y.field1,1)))}; {((cond) => {if (!cond) throw new Error("assertion failed: y.field3.field0 == \"foo\"")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(y.field3.field0,"foo")))}; + const lotsOfTypes = require("./lotsOfTypes.Struct.js")($stdlib.std.Struct, $stdlib.core.NodeJsCode.fromInline); + const Razzle = require("./Razzle.Struct.js")($stdlib.std.Struct, $stdlib.core.NodeJsCode.fromInline); + const Dazzle = require("./Dazzle.Struct.js")($stdlib.std.Struct, $stdlib.core.NodeJsCode.fromInline); + const Showtime = require("./Showtime.Struct.js")($stdlib.std.Struct, $stdlib.core.NodeJsCode.fromInline); const s = ({"a": "Boom baby"}); this.node.root.new("@winglang/sdk.std.Test",std.Test,this,"test:struct definitions are phase independant",new $Closure1(this,"$Closure1")); } diff --git a/tools/hangar/src/generated_test_targets.ts b/tools/hangar/src/generated_test_targets.ts index ebf69b123f7..7adc96848e9 100644 --- a/tools/hangar/src/generated_test_targets.ts +++ b/tools/hangar/src/generated_test_targets.ts @@ -38,11 +38,11 @@ export async function compileTest( // which files to include from the .wing directory const dotWing = join(targetDir, ".wing"); - const include = ["preflight.", "inflight.", "extern/", "proc"]; + const include = ["preflight.", "inflight.", "extern/", "proc", ".Struct.js"]; for await (const dotFile of walkdir(dotWing)) { const subpath = relative(dotWing, dotFile).replace(/\\/g, "/"); - if (!include.find((f) => subpath.startsWith(f))) { + if (!include.find((f) => subpath.includes(f))) { continue; } let fileContents = await fs.readFile(dotFile, "utf8");