diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d945965 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,51 @@ +# Repository Guidelines + +## Project Structure & Module Organization +- `latest/cls/pkg/isc/json/`: Core IRIS ObjectScript sources (adaptor, mapping, utils). +- `latest/internal/testing/unit_tests/UnitTest/isc/json/`: Unit tests using `%UnitTest.TestCase`. +- `latest/docs/` and `latest/README.md`: User docs and notes. +- `latest/module.xml`: ZPM module manifest for packaging/testing. + +## Source Control +- Perforce is used instead of git. +- If a file is not checked out in perforce, run p4 edit or p4 add as needed before making edits + +## Build, Test, and Development Commands +- Compile sources (from IRIS terminal in the correct namespace): + - `zpm "install isc.json"` +- Run tests via ZPM (prompt for instance and namespace): + - Prefer this interactive shell snippet so you can choose the IRIS instance and namespace at run time: + ```sh + read -r -p "IRIS instance name: " IRISINST + read -r -p "Namespace to run tests: " NS + iris session "$IRISINST" -U "$NS" <<'EOF' +_system +SYS +zpm "isc.json test" +halt +EOF + ``` + - To run a single test case, replace the `zpm` line with, for example: + ```sh + zpm "isc.json test -only -DUnitTest.Case=UnitTest.isc.json.exportArray" + ``` + - If already inside an IRIS terminal in your target namespace, you can simply run: + - `zpm "isc.json test"` + +## Coding Style & Naming Conventions +- One class per `.cls` file; keep class path aligned with package. +- Methods (including tests): PascalCase; avoid underscores. +- Indentation: 4 spaces or tabs consistently; align `Try/Catch`/`While` blocks. +- Use `///` for concise class/method docs; prefer code examples over prose. +- Methods should throw errors instead of returning %Status unless overriding a method that requires returning %Status. + +## Testing Guidelines +- Place tests under `latest/internal/testing/unit_tests/UnitTest/isc/json` in `UnitTest.isc.json.*` packages. +- Name test methods `Test...` and keep assertions focused and readable. +- Keep tests hermetic: no external I/O or network; use `%DynamicObject`/`%DynamicArray` fixtures. +- Run tests locally before pushing; ensure new tests pass and do not break existing ones. + +## Security & Configuration Tips +- Do not include real data/PII in fixtures. +- Prefer configuration via parameters over hardcoding environment paths. +- Validate JSON with `%DynamicAbstractObject` APIs; avoid unsafe string parsing. diff --git a/CHANGELOG.md b/CHANGELOG.md index 24a7034..a322d75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,81 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.5.0] - 2025-09-18 + +### Added +- HSIEO-13278: Add %JSONImportArray, %JSONExportToDynamicObject and %JSONExportArray. + +### Changed +- HSIEO-13278: Refactor %JSONExport* methods to export to %DynamicObject and have other %JSONExport* methods call out to it. Update datatypes with JSONTYPE of runtime since that is now updated to not need extra escaping for LogicalToJSON(). + +### Fixed +- HSIEO-13278: Fix bug where invalid mapping would use base mapping. Throw error instead +- HSIEO-13655: Fix ILLEGAL VALUE in export generation due to invalid third arg to %Set() - caused by HSIEO-13278. + +## [3.4.1] - 2025-09-03 + +### Fixed +- HSIEO-13346: Fix %JSONNewDefault handling to avoid METHOD DOES NOT EXIST errors + +## [3.4.0] - 2025-08-22 + +### Added +- HSIEO-13279: Add new code generated method %JSONNewDefault. This will do default handling for %JSONNew +which is to return %New() of the corresponding class if not persistent. +If persistent, try to match against ID or unique indices based on what is available +(ID will override unique index if both are present). +Update user guide and readme. + +## [3.3.0] - 2025-08-06 + +### Added +- HSIEO-12322: New JSONTYPE of runtime to dynamically determine JSON type +- HSIEO-10522: Add IDField and IncludeID in jsonMappingInfo + +### Changed +- HSIEO-13080: Add Author info to module from "Ownership of AppModules" confluence page + +## [3.2.1] - 2024-07-01 + +### Fixed +- APPS-23837: Array property keys are not properly escaped by generated %JSONExport() code +- APPS-23826: Fix bug in camelCase conversion when second char is a number. +- HSIEO-10881: Fix bug in json generator dynamic object import. + +## [3.2.0] - 2024-04-10 + +### Added +- APPS-21020: New method %JSONMappingInfo which returns the parsed mapping info for a given JSON mapping given the mapping name. +Useful for creating utilities that rely on the JSON mapping metadata + +## [3.1.0] - 2023-10-18 + +### Changed +- HSIEO-5398: Ensure `IncludeID` default matches `%JSONINCLUDEID` default of 1. + +## [3.0.0] - 2023-09-27 + +### Changed +- HSIEO-8297: IPM Adoption +- HSIEO-9269, HSIEO-9402: Deprecate % in perforce path + +## [2.2.2] - 2023-09-15 + +## [2.2.1] - 2023-06-03 + +## [2.2.0] - 2023-08-16 + +### Fixed +- APPS-20986: Ensure `IsValid()` errors are returned if import fails +rather than generic JSON import error which obscures away error details. + +## [2.1.0] - 2023-03-08 + +### Added +- APPS-12974: Add support for %DynamicObject/Array properties, then remove usage of %Extends against +the class being compiled. + ## [2.0.1] - 2024-12-04 ### Fixed @@ -27,7 +102,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - APPS-13390: All `%pkg.isc.json.dataType.*` classes - APPS-13390: Overridden error macros for working on Cache (now only supported on IRIS so not needed) - ## [1.0.0] - 2022-04-21 - Last released version before CHANGELOG existed. - diff --git a/README.md b/README.md index 35eb15f..b0da3d4 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,13 @@ Note: a minimum platform version of InterSystems IRIS 2018.1 is required. ### Installation: ZPM -If you already have the [ObjectScript Package Manager](https://openexchange.intersystems.com/package/ObjectScript-Package-Manager-2), installation is as easy as: +If you already have the [InterSystems Package Manager](https://github.com/intersystems/ipm), installation is as easy as: ``` zpm "install isc.json" ``` ## User Guide -See [isc.json User Guide](https://github.com/intersystems/isc-json/blob/master/docs/user-guide.md). +See [isc.json User Guide](./docs/user-guide.md). ## Support If you find a bug or would like to request an enhancement, [report an issue](https://github.com/intersystems/isc-json/issues/new). If you have a question, feel free to post it on the [InterSystems Developer Community](https://community.intersystems.com/). @@ -29,4 +29,4 @@ We use [SemVer](http://semver.org/) for versioning. Declare your dependencies us See also the list of [contributors](https://github.com/intersystems/isc-json/graphs/contributors) who participated in this project. ## License -This project is licensed under the MIT License - see the [LICENSE](https://github.com/intersystems/isc-json/blob/master/LICENSE) file for details. +This project is licensed under the MIT License - see the [LICENSE](https://github.com/intersystems/isc-json/blob/master/LICENSE) file for details. \ No newline at end of file diff --git a/cls/_pkg/isc/json/adaptor.cls b/cls/_pkg/isc/json/adaptor.cls index 7bc100c..670cadd 100644 --- a/cls/_pkg/isc/json/adaptor.cls +++ b/cls/_pkg/isc/json/adaptor.cls @@ -3,11 +3,11 @@ Include (%occInclude, %callout, %pkg.isc.json.map) IncludeGenerator %occInclude /// The following table displays the correspondence between JSON field values and -/// property values that will be implemented by the JSON Adaptor.

+/// property values that will be implemented by the JSON Adaptor.

/// /// The actual conversion between registered object values and JSON values will be done by new datatype methods: JSONToLogical and LogicalToJSON. /// In addition, a new JSONTYPE parameter will be introduced for the datatype classes -/// which indicates how the daat type will be mapped to JSON.

+/// which indicates how the data type will be mapped to JSON.

/// ///
 /// JSON                                            Registered object type
@@ -24,7 +24,7 @@ IncludeGenerator %occInclude
 /// boolean                                         %Boolean
 /// 
 /// 
-/// Note that the types %xsd package are all subclasses of base % datatypes and will map as the super type.

+/// Note that the types %xsd package are all subclasses of base % datatypes and will map as the super type.

Class %pkg.isc.json.adaptor [ Abstract, PropertyClass = %pkg.isc.json.propertyParameters, System = 3 ] { @@ -46,7 +46,7 @@ Parameter %JSONNULL As BOOLEAN = 0; /// This paramneter applies to only true strings which is determined by XSDTYPE = "string" as well as JSONTYPE="string" /// By default (%JSONIGNORENULL = 0), empty strings in the JSON input are stored as $c(0) /// and $c(0) is written to JSON as the string "". A missing field in the JSON input is always stored as "" -/// and "" is always output to JSON according to the %JSONNULL parameter.
+/// and "" is always output to JSON according to the %JSONNULL parameter.
/// If %JSONIGNORENULL is set = 1, then both missing fields in the JSON and empty strings are input as "", /// and both "" and $c(0) are output as field values of "". /// The corresponding property parameter overrides this parameter if specified. @@ -55,11 +55,11 @@ Parameter %JSONIGNORENULL As BOOLEAN = 0; /// %JSONREFERENCE specifies the default value of the %JSONREFERENCE property parameter that specifies how to /// project references to JSON. %JSONREFERENCE may be specified for any property to override this default value. /// Possible values of %JSONREFERENCE are "OBJECT", "ID", "OID", "GUID". -/// ="OBJECT" is the default and indicates that the properties of the referenced class are used to represent the referenced object.
-/// ="ID" indicates that the id of a persistent or serial class is used to represent the reference.
+/// ="OBJECT" is the default and indicates that the properties of the referenced class are used to represent the referenced object.
+/// ="ID" indicates that the id of a persistent or serial class is used to represent the reference.
/// ="OID" indicates that the oid of a persistent or serial class is used to represent the reference. -/// The form of the oid as projected to JSON will be classname,id.
-/// ="GUID" indicates the GUID of a persistent class is used to represent the reference.
+/// The form of the oid as projected to JSON will be classname,id.
+/// ="GUID" indicates the GUID of a persistent class is used to represent the reference.
/// The corresponding property parameter overrides this parameter if specified. Parameter %JSONREFERENCE As STRING [ Constraint = "OBJECT,ID,OID,GUID", Flags = ENUM ] = "OBJECT"; @@ -79,9 +79,10 @@ Parameter %JSONMAPPING As STRING; /// as camelCase. Parameter %JSONFIELDNAMEASCAMELCASE As BOOLEAN = 0; -/// %JSONImport imports JSON or dynamic object input into this object.
-/// The input argument is either JSON as a string or stream, or a subclass of %DynamicAbstractObject.
-/// mappingName is the name of the mapping to use for the import. The base mapping is represened by "" and is the default. +/// %JSONImport imports JSON or dynamic object input into this object.
+/// The input argument is either JSON as a string or stream, or a subclass of %DynamicAbstractObject.
+/// mappingName is the name of the mapping to use for the import. The base mapping is represented by "" and is the default. +/// If a mapping name is provided that does not exist, an error is thrown.
Method %JSONImport(input, %mappingName As %String = "") As %Status [ ServerOnly = 1 ] { Try { @@ -105,98 +106,136 @@ Method %JSONImport(input, %mappingName As %String = "") As %Status [ ServerOnly Quit sc } -/// Get an instance of an JSON enabled class.

+/// Import an array of JSON records into a list of objects for this class. list can be provided. +/// If not provided, one is created. Imported elements are added to the end of the list.
+/// mappingName is the name of the mapping to use for the import. The base mapping is +/// represented by "" and is the default.
+/// If a mapping name is provided that does not exist, an error is thrown.
+/// behavior has the following structure:
+///
+/// {
+/// 	// Only valid for persistant records. Ignored if class is not persistent.
+/// 	// Default: false.
+/// 	"save": boolean;
+/// 	// Whether to accumulate errors instead of quitting on the first error encountered.
+/// 	// Default: true.
+/// 	"accumulateErrors": boolean;
+/// }
+/// 
+/// errorLog contains errors that occurred indexed by the array index on which the error +/// occurred. When accumulateErrors is false, this would have at most one entry.
+/// +/// errorLog(3) = "error status from importing index element 4 (5th element) in the JSON array"
+/// errorLog(7) = "error status from importing index element 7 (8th element) in the JSON array"
+///
+ClassMethod %JSONImportArray(array As %DynamicArray, mappingName As %String = "", behavior As %DynamicObject = "", ByRef list As %ListOfObjects, Output errorLog) As %Status +{ + Set sc = $$$OK + Set totalSc = $$$OK + Kill errorLog + Try { + If '$IsObject($Get(behavior)) { + Set behavior = {} + } + Set save = behavior.%Get("save", 0) + Set accumulate = behavior.%Get("accumulateErrors", 1) + If '(($Data(list)#2) && $IsObject(list) && list.%Extends("%ListOfObjects")) { + Set list = ##class(%ListOfObjects).%New() + Set list.ElementType = $ClassName() + } + Set isPersistent = $ClassMethod($ClassName(), "%Extends", "%Persistent") + Set iter = array.%GetIterator() + While iter.%GetNext(.index, .value) { + Set iterSc = $$$OK + #dim obj As %pkg.isc.json.adaptor + Set obj = ..%JSONNew(value, , mappingName) + Set importSc = obj.%JSONImport(value, mappingName) + Set iterSc = $$$ADDSC(iterSc,importSc) + If save && isPersistent && $$$ISOK(importSc) { + Set iterSc = $$$ADDSC(iterSc,obj.%Save()) + } + Set iterSc = $$$ADDSC(iterSc,list.Insert(obj)) + // Error handling + If $$$ISERR(iterSc) { + Set errorLog(index) = iterSc + } + If (accumulate) { + Set totalSc = $$$ADDSC(totalSc,iterSc) + } Else { + $$$ThrowOnError(iterSc) + } + } + $$$ThrowOnError(totalSc) + } Catch (ex) { + Set sc = ex.AsStatus() + } + Return sc +} + +/// Get an instance of an JSON enabled class.

/// /// You may override this method to do custom processing (such as initializing /// the object instance) before returning an instance of this class. -/// However, this method should not be called directly from user code.
-/// Arguments:
-/// dynamicObject is the dynamic object with thee values to be assigned to the new object.
-/// containerOref is the containing object instance when called from JSONImport. -ClassMethod %JSONNew(dynamicObject As %DynamicObject, containerOref As %RegisteredObject = "") As %RegisteredObject [ CodeMode = generator, GenerateAfter = %JSONGenerate, ServerOnly = 1 ] +/// However, this method should not be called directly from user code.
+/// Arguments:
+/// dynamicObject is the dynamic object with thee values to be assigned to the new object.
+/// containerOref is the containing object instance when called from JSONImport.
+/// mappingName is the name of the mapping to use for the export. The base mapping is represented by "" and is the default. +/// If a mapping name is provided that does not exist, an error is thrown.
+ClassMethod %JSONNew(dynamicObject As %DynamicObject, containerOref As %RegisteredObject = "", %mappingName As %String = "") As %RegisteredObject { - Quit ##class(%pkg.isc.json.generator).JSONNew(.%mode,.%class,.%property,.%method,.%parameter,.%codemode,.%code,.%classmodify,.%context) + Return ..%JSONNewDefault(dynamicObject, containerOref, %mappingName) } -/// Serialize a JSON enabled class as a JSON document and write it to the current device.
-/// mappingName is the name of the mapping to use for the export. The base mapping is represened by "" and is the default. +/// Serialize a JSON enabled class as a JSON document and write it to the current device.
+/// mappingName is the name of the mapping to use for the export. The base mapping is represented by "" and is the default. +/// If a mapping name is provided that does not exist, an error is thrown.
Method %JSONExport(%mappingName As %String = "") As %Status { + Set sc=$$$OK Try { - Set sc=$$$OK - // Do the export to current device now. - Set sc=..%JSONExportInternal() - + $$$ThrowOnError(..%JSONExportToDynamicObject(.json, %mappingName)) + Do json.%ToJSON() } Catch ex { Set sc=ex.AsStatus() } - - Quit sc + Return sc } -/// Serialize a JSON enabled class as a JSON document and write it to a stream.
-/// mappingName is the name of the mapping to use for the export. The base mapping is represened by "" and is the default. +/// Serialize a JSON enabled class as a JSON document and write it to a stream.
+/// mappingName is the name of the mapping to use for the export. The base mapping is represetned by "" and is the default. +/// If a mapping name is provided that does not exist, an error is thrown.
Method %JSONExportToStream(ByRef export As %Stream.Object, %mappingName As %String = "") As %Status { - // Save current device - Set io=$io - + Set sc = $$$OK Try { // Always output to %FileCharacterStream If $get(export)="" { - Set export=##class(%FileCharacterStream).%New() + Set export=##class(%Stream.FileCharacter).%New() // JSON is always UTF-8 Set export.TranslateTable="UTF8" Set filestream=1 - } Else { - Set filestream = ($zobjclass(export)="%Library.FileCharacterStream") - } - If filestream { - Set stream=export - } Else { - Set stream=##class(%FileCharacterStream).%New() - Set stream.TranslateTable="UTF8" - } - - // Force stream's file to open - Set sc=stream.Write("") - - // Export JSON to the stream - If $$$ISOK(sc) { - Set file=stream.Filename ; get filename and make current device - Use file:(/NOXY) - Set sc=..%JSONExportInternal() - // Don't Close file to leave stream positioned - Use io } - - // Need to ensdure that LineTerminator is correct for the platform - If $$$ISOK(sc) Set stream.LineTerminator=$select($$$isUNIX:$char(10),1:$char(13,10)) - - // If we created the stream and caller passed in stream, then copy it to the caller's stream - If 'filestream,$$$ISOK(sc) { - Set sc=export.CopyFrom(stream) - } - + $$$ThrowOnError(..%JSONExportToDynamicObject(.json, %mappingName)) + Do json.%ToJSON(.export) } Catch ex { Set sc=ex.AsStatus() } - - Quit sc + Return sc } -/// Serialize a JSON enabled class as a JSON document and return it as a string.
-/// mappingName is the name of the mapping to use for the export. The base mapping is represened by "" and is the default. -Method %JSONExportToString(ByRef %export As %String, %mappingName As %String = "") As %Status +/// Serialize a JSON enabled class as a JSON document and return it as a string.
+/// mappingName is the name of the mapping to use for the export. The base mapping is represented by "" and is the default. +/// If a mapping name is provided that does not exist, an error is thrown.
+Method %JSONExportToString(ByRef export As %String, %mappingName As %String = "") As %Status { + Set sc=$$$OK Try { - Set sc=$$$OK - // Do the export to current device now. - Set %export="" - Set sc=..%JSONExportToStringInternal() - + Set export="" + $$$ThrowOnError(..%JSONExportToDynamicObject(.json, %mappingName)) + Set export = json.%ToJSON() } Catch ex { If ex.Name="" { Set sc=$$$ERROR($$$JSONMaxString,$$$mappingNameDisplay(%mappingName)) @@ -204,8 +243,230 @@ Method %JSONExportToString(ByRef %export As %String, %mappingName As %String = " Set sc=ex.AsStatus() } } + Return sc +} - Quit sc +/// Serialize a JSON enabled class as a JSON document and return it as JSON.
+/// mappingName is the name of the mapping to use for the export. The base mapping is reprtesened by "" and is the default. +/// If a mapping name is provided that does not exist, an error is thrown.
+Method %JSONExportToDynamicObject(Output %export As %DynamicObject, %mappingName As %String = "") As %Status +{ + Set sc = $$$OK + Try { + Set %export = {} + Do ..%JSONExportInternal() + } Catch (ex) { + Set sc = ex.AsStatus() + } + Return sc +} + +/// Export the provided list to a JSON array. array can be provided. Entries are appended +/// to the end of the array.
+/// The list can be one of the following:
+/// If the class is NOT persistent:
+/// +/// If the class is persistent:
+/// +/// mappingName is the name of the mapping to get metadata for. +/// The base mapping is represented by "" and is the default. +/// If a mapping name is provided that does not exist, an error is thrown.
+/// behavior has the following structure:
+///
+/// {
+/// 	// Whether to accumulate errors instead of quitting on the first error encountered.
+/// 	// Default: true.
+/// 	"accumulateErrors": boolean;
+/// }
+/// 
+/// errorLog contains errors that occurred indexed by the list index on which the error +/// occurred. When accumulateErrors is false, this would have at most one entry.
+/// +/// errorLog(3) = "error status from importing index element 4 (5th element) in the JSON array"
+/// errorLog(7) = "error status from importing index element 7 (8th element) in the JSON array"
+///
+ClassMethod %JSONExportArray(list As %RawString, mappingName As %String = "", behavior As %DynamicObject = "", ByRef array As %DynamicArray, Output errorLog) As %Status +{ + #dim objList as %ListOfObjects + #dim idList as %ListOfDataTypes + #dim obj as %pkg.isc.json.adaptor + #dim arrList as %DynamicArray + #dim rs as %SQL.StatementResult + Set invalidInputError = $$$ERROR($$$JSONInvalidListForExportArray,$ClassName()) + Set sc = $$$OK + Set totalSc = $$$OK + Kill errorLog + Try { + Set isPersistent = $ClassMethod($ClassName(),"%Extends","%Persistent") + If '$IsObject($Get(array)) { + Set array = [] + } + If '$IsObject($Get(behavior)) { + Set behavior = {} + } + Set accumulate = behavior.%Get("accumulateErrors", 1) + If $IsObject(list) { + If isPersistent && list.%Extends("%DynamicArray") { + Set arrList = list + Set iter = arrList.%GetIterator() + While iter.%GetNext(.index, .id) { + Set iterSc = $$$OK + Set obj = $ClassMethod($ClassName(), "%OpenId", id, 0, .sc) + Set iterSc = $$$ADDSC(iterSc,sc) + If $IsObject(obj) { + Set iterSc = $$$ADDSC(iterSc,obj.%JSONExportToDynamicObject(.json,mappingName)) + If $$$ISOK(iterSc) { + Do array.%Push(json) + } + } + // Clear up memory + Kill obj + // Error handling + If $$$ISERR(iterSc) { + Set errorLog(index) = iterSc + } + If (accumulate) { + Set totalSc = $$$ADDSC(totalSc,iterSc) + } Else { + $$$ThrowOnError(iterSc) + } + } + } ElseIf list.%Extends("%ListOfObjects") { + // Only one that supports non-persistent + Set objList = list + For i = 1:1:objList.Count() { + Set iterSc = $$$OK + Set obj = objList.GetAt(i) + Set iterSc = $$$ADDSC(iterSc,obj.%JSONExportToDynamicObject(.json,mappingName)) + If $$$ISOK(iterSc) { + Do array.%Push(json) + } + // Clear up memory + Kill obj + // Error handling + If $$$ISERR(iterSc) { + Set errorLog(i-1) = iterSc + } + If (accumulate) { + Set totalSc = $$$ADDSC(totalSc,iterSc) + } Else { + $$$ThrowOnError(iterSc) + } + } + } ElseIf isPersistent && list.%Extends("%ListOfDataTypes") { + Set idList = list + For i = 1:1:idList.Count() { + Set iterSc = $$$OK + Set id = idList.GetAt(i) + Set obj = $ClassMethod($ClassName(), "%OpenId", id, 0, .sc) + Set iterSc = $$$ADDSC(iterSc,sc) + If $IsObject(obj) { + Set iterSc = $$$ADDSC(iterSc,obj.%JSONExportToDynamicObject(.json,mappingName)) + If $$$ISOK(iterSc) { + Do array.%Push(json) + } + } + // Clear up memory + Kill obj + // Error handling + If $$$ISERR(iterSc) { + Set errorLog(i-1) = iterSc + } + If (accumulate) { + Set totalSc = $$$ADDSC(totalSc,iterSc) + } Else { + $$$ThrowOnError(iterSc) + } + } + } ElseIf isPersistent && list.%Extends("%SQL.StatementResult") { + Set rs = list + Set index = 0 + While rs.%Next(.sc) { + Set iterSc = $$$OK + Set id = rs.%Get("ID") + If (id = "") { + Continue + } + Set obj = $ClassMethod($ClassName(), "%OpenId", id, 0, .sc) + Set iterSc = $$$ADDSC(iterSc,sc) + If $IsObject(obj) { + Set iterSc = $$$ADDSC(iterSc,obj.%JSONExportToDynamicObject(.json,mappingName)) + If $$$ISOK(iterSc) { + Do array.%Push(json) + } + } + // Clear up memory + Kill obj + // Error handling + If $$$ISERR(iterSc) { + Set errorLog(index) = iterSc + } + If (accumulate) { + Set totalSc = $$$ADDSC(totalSc,iterSc) + } Else { + $$$ThrowOnError(iterSc) + } + Do $Increment(index) + } + } Else { + $$$ThrowStatus(invalidInputError) + } + } ElseIf isPersistent && $ListValid(list) { + Set ptr = 0 + Set index = 0 + While $ListNext(list,ptr,id) { + Set iterSc = $$$OK + Set obj = $ClassMethod($ClassName(), "%OpenId", id, 0, .sc) + Set iterSc = $$$ADDSC(iterSc,sc) + If $IsObject(obj) { + Set iterSc = $$$ADDSC(iterSc,obj.%JSONExportToDynamicObject(.json,mappingName)) + If $$$ISOK(iterSc) { + Do array.%Push(json) + } + } + // Clear up memory + Kill obj + // Error handling + If $$$ISERR(iterSc) { + Set errorLog(index) = iterSc + } + If (accumulate) { + Set totalSc = $$$ADDSC(totalSc,iterSc) + } Else { + $$$ThrowOnError(iterSc) + } + Do $Increment(index) + } + } Else { + $$$ThrowStatus(invalidInputError) + } + // Throw on error at the end + $$$ThrowOnError(totalSc) + } Catch (ex) { + Set sc = ex.AsStatus() + } + Return sc +} + +/// Returns metadata about a provided JSON mapping for the current JSON serializable class.
+/// mappingName is the name of the mapping to get metadata for. The base mapping is represented by "" and is the default. +ClassMethod %JSONMappingInfo(Output mappingInfo As %pkg.isc.json.mappingInfo, %mappingName As %String = "") As %Status +{ + Set sc = $$$OK + Try { + Set mappingInfo = ..%JSONMappingInfoInternal() + } Catch (ex) { + Set sc = ex.AsStatus() + } + Return sc } /// Dummy method which exists to force GenerateAfter code to take affect for %JSONGenerate. @@ -235,16 +496,23 @@ Method %JSONImportInternal() As %Status [ CodeMode = generator, GenerateAfter = } /// %JSONExportInternal is internal method used by %JSONExport -Method %JSONExportInternal() As %Status [ CodeMode = generator, GenerateAfter = %JSONGenerate, Internal, ServerOnly = 1 ] +Method %JSONExportInternal() [ CodeMode = generator, GenerateAfter = %JSONGenerate, Internal, ServerOnly = 1 ] { Quit ##class(%pkg.isc.json.generator).JSONExportInternal(.%mode,.%class,.%property,.%method,.%parameter,.%codemode,.%code,.%classmodify,.%context) } -/// %JSONExportToStringInternal is internal method used by %JSONExport -Method %JSONExportToStringInternal() As %Status [ CodeMode = generator, GenerateAfter = %JSONGenerate, Internal, ServerOnly = 1 ] +/// Internal method used by %JSONMappingInfo +ClassMethod %JSONMappingInfoInternal() As %pkg.isc.json.mappingInfo [ CodeMode = generator, GenerateAfter = %JSONGenerate, Internal, ServerOnly = 1 ] { - Quit ##class(%pkg.isc.json.generator).JSONExportToStringInternal(.%mode,.%class,.%property,.%method,.%parameter,.%codemode,.%code,.%classmodify,.%context) + Quit ##class(%pkg.isc.json.generator).JSONMappingInfo(.%mode,.%class,.%property,.%method,.%parameter,.%codemode,.%code,.%classmodify,.%context) } +/// Generates default handling for JSONNew implementation. If overriding %JSONNew, +/// this can be called to retain the generated default handling (similar to calling super for non-generated +/// but inherited methods) +ClassMethod %JSONNewDefault(dynamicObject As %DynamicObject, containerOref As %RegisteredObject = "", %mappingName As %String = "") As %RegisteredObject [ CodeMode = generator, GenerateAfter = %JSONGenerate ] +{ + Quit ##class(%pkg.isc.json.generator).JSONNewDefault(.%mode,.%class,.%property,.%method,.%parameter,.%codemode,.%code,.%classmodify,.%context) } +} diff --git a/cls/_pkg/isc/json/dataType/list.cls b/cls/_pkg/isc/json/dataType/list.cls index d632bfc..b147d32 100644 --- a/cls/_pkg/isc/json/dataType/list.cls +++ b/cls/_pkg/isc/json/dataType/list.cls @@ -18,7 +18,7 @@ ClassMethod LogicalToJSON(val As %List) As %String Do array.%Push("","null") } } - Quit array.%ToJSON() + Quit array } /// Converts the SOAP encoded input list value into an ObjectScript $list value. diff --git a/cls/_pkg/isc/json/generator.cls b/cls/_pkg/isc/json/generator.cls index 8d602a3..0581c17 100644 --- a/cls/_pkg/isc/json/generator.cls +++ b/cls/_pkg/isc/json/generator.cls @@ -73,7 +73,7 @@ ClassMethod GenerateMapping(class As %String, Output mapping) As %Status [ Inter Do ..GetMemberInfo(class,property,.includeProperty,.type,.membercat,.objJSONEnabled) If includeProperty { - // Default for %JSONINCLUDE is "inout" + // Default for %JSONINCLUDE is "inout" If jsoninclude="" Set jsoninclude="inout" // Do not support array of streams @@ -94,14 +94,13 @@ ClassMethod GenerateMapping(class As %String, Output mapping) As %Status [ Inter Quit } } ElseIf membercat["LITERAL" { - Set literaltype=$$$comMemberKeyGet(type,$$$cCLASSparameter,"JSONTYPE",$$$cPARAMdefault) - If literaltype="" Set literaltype="string" - - // We need to differentiate between registered object strings and JSON strings. - // For example between %String and %Timestamp. - // A string is considered a registered object string if XSDTYPE, as well as JSONTYPE, is = "string". - If literaltype="string",$$$comMemberKeyGet(type,$$$cCLASSparameter,"XSDTYPE",$$$cPARAMdefault)'="string" { - Set literaltype="string/json" + // Allow property param level override of JSONTYPE + Set literaltype=$$$comMemberArrayGet(class,$$$cCLASSproperty,property,$$$cPROPparameter,"%JSONTYPE") + If (literaltype="") { + Set literaltype=$$$comMemberKeyGet(type,$$$cCLASSparameter,"JSONTYPE",$$$cPARAMdefault) + } + If (literaltype="") { + Set literaltype="string" } } @@ -189,7 +188,7 @@ ClassMethod GenerateMapping(class As %String, Output mapping) As %Status [ Inter // Copy nodes to map ordered by Sequence Number If $$$ISOK(sc) { - Set mapping($c(1),0)=$lb(class,jsonignoreinvalidfield) + Set mapping($c(1),0)=$lb(class,jsonignoreinvalidfield,jsonincludeid,jsonidfield) Set mapping($c(1))=0 Set cls="" @@ -253,12 +252,15 @@ ClassMethod GetMemberInfo(class As %String, property As %String, ByRef includePr If includeProperty { Set type=$$$comMemberKeyGet(class,$$$cCLASSproperty,property,$$$cPROPtype) - If type="" Set type="%Library.String" - Set typecat=$$$getClassType(type) - - // Get member category for base type - Set membercat=##class(%Compiler.XML.Generator.Adaptor).TypeToMembercat(type,typecat,.mixed) - + if (type ="%Library.DynamicObject") || (type ="%Library.DynamicArray") || (type ="%Library.DynamicAbstractObject") { + set membercat = "DYNAMIC" + } else { + If type="" Set type="%Library.String" + Set typecat=$$$getClassType(type) + + // Get member category for base type + Set membercat=##class(%Compiler.XML.Generator.Adaptor).TypeToMembercat(type,typecat,.mixed) + } // If no membercat, then do not include property If membercat="" Set includeProperty=0 } @@ -272,7 +274,7 @@ ClassMethod GetMemberInfo(class As %String, property As %String, ByRef includePr } } Else { If collection="" { - // Not a collection - LITERAL|OBJPERSISTENT|OBJSERIAL|OBJREGISTERED + // Not a collection - LITERAL|OBJPERSISTENT|OBJSERIAL|OBJREGISTERED|DYNAMIC If (membercat'="LITERAL") { // Get list of super classes Kill typeSeq @@ -570,6 +572,8 @@ ClassMethod GenerateMappingFromClass(class As %String, name As %String, obj As % // Get %JSONIGNOREINVALIDFIELD parameter Set jsonignoreinvalidfield=obj.IgnoreInvalidField + Set jsonincludeid=obj.IncludeID + Set jsonidfield=obj.IDField // Get %JSONMAPPING class parameter. Set jsondefaultmapping=obj.Mapping @@ -591,7 +595,7 @@ ClassMethod GenerateMappingFromClass(class As %String, name As %String, obj As % Set jsonfieldnameascamelcase=''obj.FieldNameAsCamelCase // Save mappng parameterd for entire class - Set mapping(name,0)=$lb(class,jsonignoreinvalidfield) + Set mapping(name,0)=$lb(class,jsonignoreinvalidfield,jsonincludeid,jsonidfield) // Special case: ID If ''obj.IncludeID && ($$$comClassKeyGet(class,$$$cCLASSclasstype) = $$$cCLASSCLASSTYPEPERSISTENT) { @@ -659,13 +663,6 @@ ClassMethod GenerateMappingFromClass(class As %String, name As %String, obj As % } ElseIf membercat["LITERAL" { Set literaltype=$$$comMemberKeyGet(type,$$$cCLASSparameter,"JSONTYPE",$$$cPARAMdefault) If literaltype="" Set literaltype="string" - - // We need to differentiate between registered object strings and JSON strings. - // For example between %String and %Timestamp. - // A string is considered a registered object string if XSDTYPE, as well as JSONTYPE, is = "string". - If literaltype="string",$$$comMemberKeyGet(type,$$$cCLASSparameter,"XSDTYPE",$$$cPARAMdefault)'="string" { - Set literaltype="string/json" - } } // Find if required @@ -759,21 +756,45 @@ ClassMethod %JSONGenerate(%mode, %class, %property, %method, %parameter, %codemo // Create the map from class properties If 'abstract { If $$$ISOK(sc) { - Set sc=..GenerateAllImportInternal(%class,.mapping) + Set sc=..GenerateMappingInfo(.mapping) } If $$$ISOK(sc) { - Set sc=..GenerateAllExportInternal(%class,.mapping,1) + Set sc=..GenerateAllJSONNewDefault(%class,.mapping) } If $$$ISOK(sc) { - Set sc=..GenerateAllExportInternal(%class,.mapping,0) + Set sc=..GenerateAllImportInternal(%class,.mapping) + } + + If $$$ISOK(sc) { + Set sc=..GenerateAllExportInternal(%class,.mapping) } } Quit sc } +/// Get the code for the %JSONNewDefault method to actually do the method generation +ClassMethod JSONNewDefault(%mode, %class, %property, %method, %parameter, %codemode, %code, %classmodify, %context) As %Status [ Internal ] +{ + Set %code=0 + If $$$comMemberKeyGet(%class,$$$cCLASSparameter,"%JSONENABLED",$$$cPARAMdefault) { + Merge %code=$$$tEXTcode("%JSONNewDefault") + } + Quit $$$OK +} + +/// Get the code for the %JSONMappingInfo method to actually do the method generation +ClassMethod JSONMappingInfo(%mode, %class, %property, %method, %parameter, %codemode, %code, %classmodify, %context) As %Status [ Internal ] +{ + Set %code=0 + If $$$comMemberKeyGet(%class,$$$cCLASSparameter,"%JSONENABLED",$$$cPARAMdefault) { + Merge %code=$$$tEXTcode("%JSONMappingInfo") + } + Quit $$$OK +} + /// Get the code for the %JSONImportInternal method to actually do the method generation ClassMethod JSONImportInternal(%mode, %class, %property, %method, %parameter, %codemode, %code, %classmodify, %context) As %Status [ Internal ] { @@ -804,27 +825,211 @@ ClassMethod JSONExportToStringInternal(%mode, %class, %property, %method, %param Quit $$$OK } -ClassMethod JSONNew(%mode, %class, %property, %method, %parameter, %codemode, %code, %classmodify, %context) As %Status +/// Generate the code for the %JSONNewDefault method +ClassMethod GenerateAllJSONNewDefault(class As %String, ByRef mapping As %String) As %Status [ Internal ] { + New %code Set %code=0 - If $$$comMemberKeyGet(%class,$$$cCLASSparameter,"%JSONENABLED",$$$cPARAMdefault) { - // Get default for %JSONINCLUDEID - Set jsonincludeid=''$$$comMemberKeyGet(%class,$$$cCLASSparameter,"%JSONINCLUDEID",$$$cPARAMdefault) - // Get default for %JSONIDFIELD - Set jsonidfield=$$$comMemberKeyGet(%class,$$$cCLASSparameter,"%JSONIDFIELD",$$$cPARAMdefault) - - If (($$$comClassKeyGet(%class,$$$cCLASSclasstype) = "persistent") && jsonincludeid) { - $$$GENERATE(" Set id = dynamicObject."_$$$QN(jsonidfield)) - $$$GENERATE(" If ##class("_%class_").%ExistsId(id) {") - $$$GENERATE(" Quit ##class("_%class_").%OpenId(id)") + Set sc=$$$OK + + Try { + // Process all mappings except base + Set indent=" " + Set mappingName="" + Set count=0 + For { + Set mappingName=$order(mapping(mappingName)) + If mappingName="" Quit + $$$GENERATE(" "_$select(count=0:"If",1:"} ElseIf")_" %mappingName="""_$Case(mappingName, $c(1): "", : mappingName)_""" {") + Set count=count+1 + Do ..GenerateJSONNewDefault(class,indent,mappingName,.mapping) + } + + If count>0 { $$$GENERATE(" } Else {") - $$$GENERATE(" Quit ##class("_%class_").%New()") + $$$GENERATE(" $$$ThrowStatus($$$ERROR($$$JSONInvalidMapping,%mappingName,""%JSONNewDefault"",$ClassName()))") $$$GENERATE(" }") + } + + // Save the code for this method + Merge $$$tEXTcode("%JSONNewDefault")=%code + } Catch ex { + Set sc=ex.AsStatus() + } + Quit sc +} + +/// Generate the code for the %JSONNewDefault method for the specified mappingName. +ClassMethod GenerateJSONNewDefault(class As %String, indent As %String, mappingName As %String, ByRef mapping As %String) [ Internal ] +{ + // Get default for %JSONINCLUDEID + Set jsonincludeid=$$$jsonincludeid(mapping,mappingName) + // Get default for %JSONIDFIELD + Set jsonidfield=$$$jsonidfield(mapping,mappingName) + + // Accumulate mapping of property name to field name + Kill propNameToFieldName + For index=1:1:$$$jsonpropertycount(mapping,mappingName) { + Set propertyMap = $$$jsongetprop(mapping,mappingName,index) + Set fieldName = $$$jsonfieldname(propertyMap) + Set propName = $$$jsonproperty(propertyMap) + Set propNameToFieldName(propName) = fieldName + } + + If ($$$comClassKeyGet(%class,$$$cCLASSclasstype) '= $$$cCLASSCLASSTYPEPERSISTENT) { + $$$GENERATE(indent_"Return $Case(''$System.CLS.IsMthd("_$$$QUOTE(class)_", ""%New""), 1: ##class("_class_").%New(), : """")") + Return + } + // Persistent so see if we can find a record to import into + $$$GENERATE(indent_"Set id = """"") + // Iterate all indices to find a unique index + Set index = "" + Set idKeyIndexInfo = "" + While 1 { + Set index = $$$comMemberNext(class,$$$cCLASSindex,index) + If (index = "") { + Quit + } + Set idKey = $$$comMemberKeyGet(class,$$$cCLASSindex,index,$$$cINDEXidkey) + Set unique = $$$comMemberKeyGet(class,$$$cCLASSindex,index,$$$cINDEXunique) + If 'idKey && 'unique { + // Skip index since it cannot be used for unique import + Continue + } + Set props = $ListFromString($$$comMemberKeyGet(class,$$$cCLASSindex,index,$$$cINDEXproperties)) + Set args = "", condition = "" + Set ptr = 0, missingField = 0 + While $ListNext(props,ptr,prop) { + Set fieldName = $Get(propNameToFieldName(prop)) + If (fieldName = "") { + // If field isn't present in JSON import, then index cannot be used + Set missingField = 1 + Quit + } + Set arg = "dynamicObject."_$$$QN(fieldName) + Set args = args _ $ListBuild(arg) + Set condition = condition _ $ListBuild("("_arg_"'="""")") + } + If (missingField) || (args = "") { + Continue + } + If (idKey) { + // Keep track of id key to have it occur last (ensures it takes precedence) + Set idKeyIndexInfo = $ListBuild(condition,args) } Else { - $$$GENERATE(" Quit ##class("_%class_").%New()") + $$$GENERATE(indent_"If ("_$ListToString(condition, "&&")_") {") + $$$GENERATE(indent_" Do .."_index_"Exists("_$ListToString(args)_",.id)") + $$$GENERATE(indent_"}") } } - Quit $$$OK + // Use IdKey is present + If (idKeyIndexInfo '= "") { + Set $ListBuild(condition,args) = idKeyIndexInfo + $$$GENERATE(indent_"If ("_$ListToString(condition, "&&")_") {") + $$$GENERATE(indent_" Set id = "_$ListToString(args,"_""||""_")) + $$$GENERATE(indent_"}") + } + // Use ID if present + If jsonincludeid { + $$$GENERATE(indent_"Set inputId = dynamicObject."_$$$QN(jsonidfield)) + $$$GENERATE(indent_"If (inputId '= """") { Set id = inputId }") + } + // Final evaluation + $$$GENERATE(indent_"If (id '= """") && ..%ExistsId(id) {") + $$$GENERATE(indent_" Return ##class("_class_").%OpenId(id)") + $$$GENERATE(indent_"}") + $$$GENERATE(indent_"Return ##class("_class_").%New()") +} + +/// Generate the code for the %JSONMappingInfo method +ClassMethod GenerateMappingInfo(ByRef mapping As %String) As %Status [ Internal ] +{ + New %code + Set %code = 0 + Set sc = $$$OK + #define Generate(%line) $$$GENERATE($Char(9)_%line) + Try { + Set returnObjName = "obj" + Set propertyObjName = "prop" + $$$Generate("Set "_returnObjName_" = ##class(%pkg.isc.json.mappingInfo).%New()") + Set mappingName = "" + Set isFirst = 1 + While 1 { + Set mappingName = $Order(mapping(mappingName)) + If (mappingName = "") { + Quit + } + Merge mappingInfo = mapping(mappingName) + Set isDefault = (mappingName = $Char(1)) + Set actualMappingName = mappingName + If (isDefault) { + Set actualMappingName = "" + } + If (isFirst) { + $$$Generate("If (%mappingName = "_$$$QUOTE(actualMappingName)_") {") + Set isFirst = 0 + } Else { + $$$Generate("} ElseIf (%mappingName = "_$$$QUOTE(actualMappingName)_") {") + } + If ('isDefault) { + // Add mapping name + $$$Generate($Char(9)_"Set "_returnObjName_".Mapping = "_$$$QUOTE(mappingName)) + } + Set $ListBuild(classname, ignoreInvalidField, includeID, idField) = $Get(mappingInfo(0)) + $$$Generate($Char(9)_"Set "_returnObjName_".Classname = "_$$$QUOTE(classname)) + $$$Generate($Char(9)_"Set "_returnObjName_".IgnoreInvalidField = "_ignoreInvalidField) + $$$Generate($Char(9)_"Set "_returnObjName_".IncludeID = "_includeID) + $$$Generate($Char(9)_"Set "_returnObjName_".IDField = "_$$$QUOTE(idField)) + // Iterate all properties in mapping and add them to return object + For i = 1:1:$Get(mappingInfo) { + Set $ListBuild( + propertyName, + normalizedPropertyName, + jsoninclude, + jsonfieldname, + jsonnull, + jsonignorenull, + jsonreference, + required, + literaltype, + membercat, + type, + jsonmapping + ) = $Get(mappingInfo(i)) + Set line = "Set "_propertyObjName_" = ##class(%pkg.isc.json.mappingInfo.property).%New("_ + $$$QUOTE(propertyName)_","_ + $$$QUOTE(normalizedPropertyName)_","_ + $$$QUOTE(jsoninclude)_","_ + $$$QUOTE(jsonfieldname)_","_ + jsonnull_","_ + jsonignorenull_","_ + $$$QUOTE(jsonreference)_","_ + required_","_ + $$$QUOTE(literaltype)_","_ + $$$QUOTE(membercat)_","_ + $$$QUOTE(type)_","_ + $$$QUOTE($Get(jsonmapping))_ + ")" + $$$Generate($Char(9)_line) + $$$Generate($Char(9)_"$$$ThrowOnError("_returnObjName_".Properties.Insert("_propertyObjName_"))") + } + $$$Generate($Char(9)_"Return "_returnObjName) + } + If ('isFirst) { + // Only apply final closing bracket if at least one iteration of loop + // which would toggle isFirst to 0 + $$$Generate("}") + } + + // Return "" if matched against no mapping + $$$Generate("Return """" ") + + // Save the code for this method + Merge $$$tEXTcode("%JSONMappingInfo")=%code + } Catch (ex) { + Set sc = ex.AsStatus() + } + Return sc } /// Generate the code for the %JSONImportInternal method @@ -839,28 +1044,23 @@ ClassMethod GenerateAllImportInternal(class As %String, ByRef mapping As %String Set needTestInvalidField=0 Set needRequired=0 - // Process all mappings except base - Set indent="" - Set mappingName=$c(1) + Set indent=" " + Set mappingName="" Set count=0 For { Set mappingName=$order(mapping(mappingName)) If mappingName="" Quit - $$$GENERATE(" "_$select(count=0:"If",1:"} ElseIf")_" %mappingName="""_mappingName_""" {") + $$$GENERATE(" "_$select(count=0:"If",1:"} ElseIf")_" %mappingName="""_$Case(mappingName, $c(1): "", : mappingName)_""" {") Set count=count+1 - Set indent=" " Set sc=..GenerateImportInternal(class,indent,mappingName,.mapping,.needRequired,.needTestInvalidField) - If $$$ISERR(sc) Quit + $$$ThrowOnError(sc) } - If $$$ISERR(sc) Quit - // Add in base mapping. - If $Data(mapping($c(1))) { - If count>0 $$$GENERATE(" } Else {") - Set sc=..GenerateImportInternal(class,indent,$c(1),.mapping,.needRequired,.needTestInvalidField) - If $$$ISERR(sc) Quit + If count>0 { + $$$GENERATE(" } Else {") + $$$GENERATE(" $$$ThrowStatus($$$ERROR($$$JSONInvalidMapping,%mappingName,""%JSONImportInternal"",$ClassName()))") + $$$GENERATE(" }") } - If count>0 $$$GENERATE(" }") // Generate code to check for invalid field If needTestInvalidField { @@ -1015,7 +1215,8 @@ ClassMethod GenImportField(class As %String, propertyMap As %List, indent As %St // Generate code to import character stream Do ..GenImportCharacterStream(class,propertyMap,indent,isCollectionValue) } - + } ElseIf membercat = "DYNAMIC" { + Do ..GenDynamic(class,propertyMap,indent,isCollectionValue) } Else { // Generate code to import literals Do ..GenImportLiteral(class,propertyMap,indent,isCollectionValue) @@ -1024,6 +1225,18 @@ ClassMethod GenImportField(class As %String, propertyMap As %List, indent As %St $$$GENERATE(indent_"}") } +/// Generate code for importing an %DynamicObject and storing the object reference in variable named data. +ClassMethod GenDynamic(class As %String, propertyMap As %List, indent As %String, isCollectionValue As %Integer) [ Internal ] +{ + $$$GENERATE(" Set json =%JSONObject."_ $$$jsonfieldname(propertyMap)) + set declaredType = $$$jsontype(propertyMap) + // If present, then must be the expected JSON type + $$$GENERATE(indent_" If '($IsObject(json) && json.%IsA("""_declaredType_""")) Goto %JSONImportError") + // Get the field value + set indent = indent _ " " + $$$GENERATE(indent _ "Set .." _ $$$QN($$$jsonproperty(propertyMap))_"= json") +} + /// Generate code for importing an object ID and storing the object reference in variable named data. ClassMethod GenImportID(class As %String, propertyMap As %List, indent As %String, isCollectionValue As %Integer) [ Internal ] { @@ -1032,9 +1245,9 @@ ClassMethod GenImportID(class As %String, propertyMap As %List, indent As %Strin // Get the field value If isCollectionValue { Set indexarg=$select(isCollectionValue=$$$isCollectionList:"index+1",1:"index") - $$$GENERATE(indent_" Do .."_$$$jsonpropertyQN(propertyMap)_".SetObjectIdAt(value,"_indexarg_")") + $$$GENERATE(indent_" $$$ThrowOnError(.."_$$$jsonpropertyQN(propertyMap)_".SetObjectIdAt(value,"_indexarg_"))") } Else { - $$$GENERATE(indent_" Do .."_$$$QN($$$jsonproperty(propertyMap)_"SetObjectId")_"(%JSONObject."_$$$QN($$$jsonfieldname(propertyMap))_")") + $$$GENERATE(indent_" $$$ThrowOnError(.."_$$$QN($$$jsonproperty(propertyMap)_"SetObjectId")_"(%JSONObject."_$$$QN($$$jsonfieldname(propertyMap))_"))") } } @@ -1048,10 +1261,10 @@ ClassMethod GenImportOID(class As %String, propertyMap As %List, indent As %Stri Set exp="$lb($piece("_var_","","",2,*),$piece("_var_","","",1))" If isCollectionValue { Set indexarg=$select(isCollectionValue=$$$isCollectionList:"index+1",1:"index") - $$$GENERATE(indent_" Do .."_$$$jsonpropertyQN(propertyMap)_".SetObjectAt("_exp_","_indexarg_")") + $$$GENERATE(indent_" $$$ThrowOnError(.."_$$$jsonpropertyQN(propertyMap)_".SetObjectAt("_exp_","_indexarg_"))") } Else { $$$GENERATE(indent_" Set data=%JSONObject."_$$$QN($$$jsonfieldname(propertyMap))) - $$$GENERATE(indent_" Do .."_$$$QN($$$jsonproperty(propertyMap)_"SetObject")_"("_exp_")") + $$$GENERATE(indent_" $$$ThrowOnError(.."_$$$QN($$$jsonproperty(propertyMap)_"SetObject")_"("_exp_"))") } } @@ -1063,9 +1276,9 @@ ClassMethod GenImportGUID(class As %String, propertyMap As %List, indent As %Str // Get the field value If isCollectionValue { Set indexarg=$select(isCollectionValue=$$$isCollectionList:"index+1",1:"index") - $$$GENERATE(indent_" Do .."_$$$jsonpropertyQN(propertyMap)_".SetObjectAt(##class(%Library.GUID).%GUIDFind(value),"_indexarg_")") + $$$GENERATE(indent_" $$$ThrowOnError(.."_$$$jsonpropertyQN(propertyMap)_".SetObjectAt(##class(%Library.GUID).%GUIDFind(value),"_indexarg_"))") } Else { - $$$GENERATE(indent_" Do .."_$$$QN($$$jsonproperty(propertyMap)_"SetObject")_"(##class(%Library.GUID).%GUIDFind(%JSONObject."_$$$QN($$$jsonfieldname(propertyMap))_"))") + $$$GENERATE(indent_" $$$ThrowOnError(.."_$$$QN($$$jsonproperty(propertyMap)_"SetObject")_"(##class(%Library.GUID).%GUIDFind(%JSONObject."_$$$QN($$$jsonfieldname(propertyMap))_")))") } } @@ -1075,20 +1288,25 @@ ClassMethod GenImportObject(class As %String, propertyMap As %List, indent As %S Set mappingName=$$$jsonmapping(propertyMap) // If present, then must be the expected JSON type $$$GENERATE(indent_" If jsontype=""object"" {") - // Get a new empty object + // Get a new starting object $$$GENERATE(indent_" Set saveJSON=%JSONObject") $$$GENERATE(indent_" Set %JSONObject="_$select(isCollectionValue:"value",1:"%JSONObject."_$$$QN($$$jsonfieldname(propertyMap)))) - $$$GENERATE(indent_" Set newobj=##class("_$$$jsontype(propertyMap)_").%JSONNew(%JSONObject,$this)") - // Get the field value - If mappingName'="" { - $$$GENERATE(indent_" Set saveMapping=%mappingName,%mappingName="""_mappingName_"""") - } + $$$GENERATE(indent_" Set saveMapping=%mappingName,%mappingName="""_mappingName_"""") + // Call %JSONNew with either 3 args or 2 for backwards compatibility based on whether it + // supports mappingName as the third arg + $$$GENERATE(indent_" Set formalSpecLen=$ListLength($$$comMemberKeyGet("_$$$QUOTE($$$jsontype(propertyMap))_",$$$cCLASSmethod,""%JSONNew"",$$$cMETHformalspecparsed))") + $$$GENERATE(indent_" If (formalSpecLen = 3) {") + $$$GENERATE(indent_" Set newobj=##class("_$$$jsontype(propertyMap)_").%JSONNew(%JSONObject,$this,%mappingName)") + $$$GENERATE(indent_" } Else {") + $$$GENERATE(indent_" Set newobj=##class("_$$$jsontype(propertyMap)_").%JSONNew(%JSONObject,$this)") + $$$GENERATE(indent_" }") // Call adapter for referenced object. - $$$GENERATE(indent_" Set sc=newobj.%JSONImportInternal()") + $$$GENERATE(indent_" Set sc=$$$OK") + $$$GENERATE(indent_" If $IsObject($Get(newobj)) {") + $$$GENERATE(indent_" Set sc=newobj.%JSONImportInternal()") + $$$GENERATE(indent_" }") $$$GENERATE(indent_" Set %JSONObject=saveJSON") - If mappingName'="" { - $$$GENERATE(indent_" Set %mappingName=saveMapping") - } + $$$GENERATE(indent_" Set %mappingName=saveMapping") $$$GENERATE(indent_" If $$$ISERR(sc) Goto %JSONImportExit") If isCollectionValue=$$$isCollectionList { // Generate code to save value in list @@ -1193,8 +1411,15 @@ ClassMethod GenImportLiteral(class As %String, propertyMap As %List, indent As % // If present, then must be the expected JSON type If literaltype="double" { $$$GENERATE(indent_" If jsontype'=""number"",jsontype'=""string"" Goto %JSONImportError") + } ElseIf literaltype="runtime" { + Set methodName="GetJSONTYPE" + Set haveGetJsontype=$$$defMemberDefined(type,$$$cCLASSmethod,methodName) + If 'haveGetJsontype { + $$$ThrowStatus($$$ERROR($$$GeneralError,$$$FormatText("With JSONTYPE of runtime, %1() method MUST be defined for data type %2 on property '%3'",methodName,type,property))) + } + $$$GENERATE(indent_" If jsontype'=##class("_type_")."_methodName_"(%JSONObject."_$$$QN($$$jsonfieldname(propertyMap))_") Goto %JSONImportError") } Else { - $$$GENERATE(indent_" If jsontype'="""_$piece(literaltype,"/",1)_""" Goto %JSONImportError") + $$$GENERATE(indent_" If jsontype'="""_literaltype_""" Goto %JSONImportError") } // Get the field value If isCollectionValue { @@ -1214,6 +1439,9 @@ ClassMethod GenImportLiteral(class As %String, propertyMap As %List, indent As % Set haveJSONToLogical=##class(%Compiler.XML.Generator.Adaptor).getSingleLine(class,property,type,"XSDToLogical",.parms,var,.codeJSONToLogical) } Set haveIsValid=##class(%Compiler.XML.Generator.Adaptor).getSingleLine(class,property,type,"IsValid",.parms,var,.codeIsValid) + If (literaltype="runtime") && '(haveIsValid && haveJSONToLogical) { + $$$ThrowStatus($$$ERROR($$$GeneralError,$$$FormatText("With JSONTYPE of runtime, IsValid() and JSONToLogical() methods MUST be defined for data type %1 on property '%2'",type,property))) + } // Call datatype methods Set line="" @@ -1221,7 +1449,8 @@ ClassMethod GenImportLiteral(class As %String, propertyMap As %List, indent As % Set line=line_" Set "_var_"="_codeJSONToLogical_" Goto:"_var_"="""" %JSONImportError" } If haveIsValid { - Set line=line_" If $$$ISERR("_codeIsValid_") Goto %JSONImportError" + Set line=line_" Set sc = "_codeIsValid + Set line=line_" If $$$ISERR(sc) Goto %JSONImportExit" } If line'="" { If useProperty { @@ -1253,55 +1482,33 @@ ClassMethod GenImportLiteral(class As %String, propertyMap As %List, indent As % } /// Generate the code for the %JSONExportInternal method -ClassMethod GenerateAllExportInternal(class As %String, ByRef mapping As %String, useWrite As %Boolean) As %Status [ Internal ] +ClassMethod GenerateAllExportInternal(class As %String, ByRef mapping As %String) As %Status [ Internal ] { - New %code,%outputCode,%exitCode,%objectCode + New %code Set %code=0 + // Comes from %JSONExportToDynamicObject + Set %exportVar = "%export" Set sc=$$$OK - Try { - // Setup the output code for write or string concatenation. - If useWrite { - Set %outputCode="Write " - Set %exitCode="%JSONExportExit" - Set %objectCode="%JSONExportInternal" - } Else { - Set %outputCode="Set %export=%export_" - Set %exitCode="%JSONExportExitToString" - Set %objectCode="%JSONExportToStringInternal" - } - - // Begin output of code - $$$GENERATE(" Set sc=$$$OK") - $$$GENERATE(" "_%outputCode_"""{"" Set sep=""""") - - // Process all mappings except base - Set indent="" - Set mappingName=$c(1) + Try { + Set indent=" " + Set mappingName="" Set count=0 For { Set mappingName=$order(mapping(mappingName)) If mappingName="" Quit - $$$GENERATE(" "_$select(count=0:"If",1:"} ElseIf")_" %mappingName="""_mappingName_""" {") + $$$GENERATE(" "_$select(count=0:"If",1:"} ElseIf")_" %mappingName="""_$Case(mappingName, $c(1): "", : mappingName)_""" {") Set count=count+1 - Set indent=" " - Set sc=..GenerateExportInternal(class,indent,mappingName,.mapping) - If $$$ISERR(sc) Quit + Do ..GenerateExportInternal(class,indent,mappingName,.mapping) } - If $$$ISERR(sc) Quit - - // Add in base mapping. - If count>0 $$$GENERATE(" } Else {") - Set sc=..GenerateExportInternal(class,indent,$c(1),.mapping) - If $$$ISERR(sc) Quit - If count>0 $$$GENERATE(" }") - - $$$GENERATE(" "_%outputCode_"""}""") - $$$GENERATE(%exitCode_" Quit sc") + If count>0 { + $$$GENERATE(" } Else {") + $$$GENERATE(" $$$ThrowStatus($$$ERROR($$$JSONInvalidMapping,%mappingName,""%JSONExportInternal"",$ClassName()))") + $$$GENERATE(" }") + } // Save the code for this method - Merge $$$tEXTcode("%JSONExport"_$select(useWrite:"",1:"ToString")_"Internal")=%code - + Merge $$$tEXTcode("%JSONExportInternal")=%code } Catch ex { Set sc=ex.AsStatus() } @@ -1326,7 +1533,7 @@ ClassMethod GenerateExportInternal(class As %String, indent As %String, mappingN Set membercat=$$$jsonmembercat(propertyMap) If membercat["Collection" { // Generate code to import collections - $$$GENERATE(" Set aval=.."_$$$jsonpropertyQN(propertyMap)) + $$$GENERATE(indent_"Set aval=.."_$$$jsonpropertyQN(propertyMap)) If membercat["List" { // Generate code to import lists Do ..GenExportList(class,propertyMap,indent) @@ -1334,14 +1541,12 @@ ClassMethod GenerateExportInternal(class As %String, indent As %String, mappingN // Generate code to import arrays Do ..GenExportArray(class,propertyMap,indent) } - } Else { // Generate code to import this non-collection field - $$$GENERATE(indent_" Set value=.."_$$$jsonpropertyQN(propertyMap)) + $$$GENERATE(indent_"Set value=.."_$$$jsonpropertyQN(propertyMap)) Do ..GenExportField(class,propertyMap,fieldname,indent,0) } } - Quit sc } @@ -1358,15 +1563,15 @@ ClassMethod GenExportList(class As %String, propertyMap As %List, indent As %Str } Else { Set nextFunction="GetNext" } - $$$GENERATE(indent_" If aval.Count()>0 {") - Do ..GenWriteField($$$jsonfieldname(propertyMap),indent_" ","") - $$$GENERATE(indent_" Set sep=""[""") - $$$GENERATE(indent_" Set k="""" For {") - $$$GENERATE(indent_" Set value=aval."_nextFunction_"(.k) If k="""" Quit") - Do ..GenExportField(class,propertyMap,""," ",$$$isCollectionList) - $$$GENERATE(indent_" }") - $$$GENERATE(indent_" "_%outputCode_"""]""") - $$$GENERATE(indent_" }") + $$$GENERATE(indent_"If aval.Count()>0 {") + $$$GENERATE(indent_" Set arr=[]") + $$$GENERATE(indent_" Set k=""""") + $$$GENERATE(indent_" For {") + $$$GENERATE(indent_" Set value=aval."_nextFunction_"(.k) If k="""" Quit") + Do ..GenExportField(class,propertyMap,"",indent_" ",$$$isCollectionList) + $$$GENERATE(indent_" }") + Do ..GenSetField($$$jsonfieldname(propertyMap),indent_" ","arr") + $$$GENERATE(indent_"}") } /// Generate code for exporting an array collection value from JSON object of the form {"key":value,...} @@ -1382,16 +1587,18 @@ ClassMethod GenExportArray(class As %String, propertyMap As %List, indent As %St } Else { Set nextFunction="GetNext" } - $$$GENERATE(indent_" If aval.Count()>0 {") - Do ..GenWriteField($$$jsonfieldname(propertyMap),indent_" ","") - $$$GENERATE(indent_" Set sep=""{""") - $$$GENERATE(indent_" Set aval=.."_$$$jsonpropertyQN(propertyMap)_",k=""""") - $$$GENERATE(indent_" For {") - $$$GENERATE(indent_" Set value=aval."_nextFunction_"(.k) If k="""" Quit") - Do ..GenExportField(class,propertyMap,$c(1)_"k",indent_" ",$$$isCollectionArray) - $$$GENERATE(indent_" }") - $$$GENERATE(indent_" "_%outputCode_"""}""") - $$$GENERATE(indent_" }") + $$$GENERATE(indent_"If aval.Count()>0 {") + $$$GENERATE(indent_" Set obj={}") + $$$GENERATE(indent_" Set k=""""") + $$$GENERATE(indent_" For {") + $$$GENERATE(indent_" Set value=aval."_nextFunction_"(.k) If k="""" Quit") + Set original = %exportVar + Set %exportVar = "obj" + Do ..GenExportField(class,propertyMap,$c(1)_"k",indent_" ",$$$isCollectionList) + Set %exportVar = original + $$$GENERATE(indent_" }") + Do ..GenSetField($$$jsonfieldname(propertyMap),indent_" ","obj") + $$$GENERATE(indent_"}") } /// Generate code for exporting a single field value. @@ -1403,77 +1610,81 @@ ClassMethod GenExportField(class As %String, propertyMap As %List, fieldName As If membercat["OBJ" { // Handle no object specified. Set serialTest=$select(membercat["SERIAL":"&&'value.%IsNull()",1:"") - $$$GENERATE(indent_" If value'="""""_serialTest_" {") - Set indent=indent_" " + $$$GENERATE(indent_"If value'="""""_serialTest_" {") + Set indentExtra = indent_" " // Output JSON for the object reference Set reference=$$$jsonreference(propertyMap) If reference=$$$jsonrefid { // Generate code to export object ID. - Do ..GenExportID(class,propertyMap,fieldName,indent,isCollectionValue) + Do ..GenExportID(class,propertyMap,fieldName,indentExtra,isCollectionValue) } ElseIf reference=$$$jsonrefoid { // Generate code to export object OID. - Do ..GenExportOID(class,propertyMap,fieldName,indent,isCollectionValue) + Do ..GenExportOID(class,propertyMap,fieldName,indentExtra,isCollectionValue) } ElseIf reference=$$$jsonrefguid { // Generate code to export object GUID. - Do ..GenExportGUID(class,propertyMap,fieldName,indent,isCollectionValue) + Do ..GenExportGUID(class,propertyMap,fieldName,indentExtra,isCollectionValue) } Else { // Generate code to export object references. - Do ..GenExportObject(class,propertyMap,fieldName,indent,isCollectionValue) + Do ..GenExportObject(class,propertyMap,fieldName,indentExtra,isCollectionValue) } - } ElseIf membercat["STREAM" { // Handle no stream specified. If $$$jsonliteraltype(propertyMap)="string",$$$jsonignorenull(propertyMap) { Set needClosingBrace=0 } Else { - $$$GENERATE(indent_" If (value'="""")&&'value.IsNull() {") - Set indent=indent_" " + $$$GENERATE(indent_"If (value'="""")&&'value.IsNull() {") } + Set indentExtra = indent_" " // Output stream If membercat="BSTREAM" { // Generate code to export binary stream - Do ..GenExportBinaryStream(class,propertyMap,fieldName,indent,isCollectionValue) + Do ..GenExportBinaryStream(class,propertyMap,fieldName,indentExtra,isCollectionValue) } Else { // Generate code to export character stream - Do ..GenExportCharacterStream(class,propertyMap,fieldName,indent,isCollectionValue) + Do ..GenExportCharacterStream(class,propertyMap,fieldName,indentExtra,isCollectionValue) } - + } ElseIf membercat = "DYNAMIC" { + Do ..GenExportDynamic(class,propertyMap,fieldName,indent,isCollectionValue) } Else { // Handle no value specified. If $$$jsonliteraltype(propertyMap)="string",$$$jsonignorenull(propertyMap) { Set needClosingBrace=0 + Set indentExtra = indent } Else { - $$$GENERATE(indent_" If value'="""" {") - Set indent=indent_" " + $$$GENERATE(indent_"If value'="""" {") + Set indentExtra = indent _ " " } // Generate code to export literals - Do ..GenExportLiteral(class,propertyMap,fieldName,indent,isCollectionValue) + Do ..GenExportLiteral(class,propertyMap,fieldName,indentExtra,isCollectionValue) } If needClosingBrace { If isCollectionValue || ($$$jsonnull(propertyMap) && '$$$jsonignorenull(propertyMap)) { $$$GENERATE(indent_"} Else {") - Do ..GenWriteField(fieldName,indent,"""null""") + Do ..GenSetField(fieldName,indent_" ","""""","""null""") $$$GENERATE(indent_"}") - $$$GENERATE(indent_"Set $extract(sep,1)="",""") } Else { - $$$GENERATE(indent_" Set $extract(sep,1)="",""") $$$GENERATE(indent_"}") } - } Else { - $$$GENERATE(indent_" Set $extract(sep,1)="",""") } } +/// Generate code for exporting a %DynamicObject as JSON. +ClassMethod GenExportDynamic(class As %String, propertyMap As %List, fieldName As %String, indent As %String, isCollectionValue As %Integer) [ Internal ] +{ + $$$GENERATE(indent_"If (value'="""") {") + Do ..GenSetField(fieldName,indent _ " ","value") +} + /// Generate code for exporting an object ID as JSON. ClassMethod GenExportID(class As %String, propertyMap As %List, fieldName As %String, indent As %String, isCollectionValue As %Integer) [ Internal ] { // Write the field value If isCollectionValue { // GetObjectNextId already returned id - Do ..GenWriteField(fieldName,indent,"""""""""_$zcvt(value,""O"",""JSON"")_""""""""") + Do ..GenSetField(fieldName,indent,"value") } Else { - Do ..GenWriteField(fieldName,indent,"""""""""_$zcvt(value.%Id(),""O"",""JSON"")_""""""""") + Do ..GenSetField(fieldName,indent,"value.%Id()") } } @@ -1483,52 +1694,49 @@ ClassMethod GenExportOID(class As %String, propertyMap As %List, fieldName As %S // Write the field value If isCollectionValue { // GetObjectNextId already returned oid - Do ..GenWriteField(fieldName,indent,"""""""""_$select($listget(value,2)="""":$listget(aval.GetAt(k).%Oid(),2),1:$listget(value,2))_"",""_$listget(value)_""""""""") + Do ..GenSetField(fieldName,indent,"$select($listget(value,2)="""":$listget(aval.GetAt(k).%Oid(),2),1:$listget(value,2))_"",""_$listget(value)") } Else { - Do ..GenWriteField(fieldName,indent,"""""""""_$listget(value.%Oid(),2)_"",""_$listget(value.%Oid())_""""""""") + Do ..GenSetField(fieldName,indent,"$listget(value.%Oid(),2)_"",""_$listget(value.%Oid())") } } /// Generate code for exporting an object GUID as JSON. ClassMethod GenExportGUID(class As %String, propertyMap As %List, fieldName As %String, indent As %String, isCollectionValue As %Integer) [ Internal ] { - Do ..GenWriteField(fieldName,indent,"""""""""_value.%GUID(value.%Oid())_""""""""") + Do ..GenSetField(fieldName,indent,"value.%GUID(value.%Oid())") } /// Generate code for exporting a referenced object as JSON. ClassMethod GenExportObject(class As %String, propertyMap As %List, fieldName As %String, indent As %String, isCollectionValue As %Integer) [ Internal ] { Set mappingName=$$$jsonmapping(propertyMap) - If mappingName'="" { - $$$GENERATE(indent_" Set saveMapping=%mappingName,%mappingName="""_mappingName_"""") - } - // Write the field value - Do ..GenWriteField(fieldName,indent,"") - $$$GENERATE(indent_" Set sc=value."_%objectCode_"() If $$$ISERR(sc) Goto "_%exitCode) - If mappingName'="" { - $$$GENERATE(indent_" Set %mappingName=saveMapping") - } + $$$GENERATE(indent_"Set mappingName="""_mappingName_"""") + $$$GENERATE(indent_"$$$ThrowOnError(value.%JSONExportToDynamicObject(.nestedJson,mappingName))") + Do ..GenSetField(fieldName,indent,"nestedJson") } /// Generate code for exporting a binary stream as JSON. ClassMethod GenExportBinaryStream(class As %String, propertyMap As %List, fieldName As %String, indent As %String, isCollectionValue As %Integer) [ Internal ] { - Do ..GenWriteField(fieldName,indent,"") If $$$jsonliteraltype(propertyMap)["hex" { - $$$GENERATE(indent_" "_%outputCode_""""""""" Do value.Rewind() If value.Size>0 { While 'value.AtEnd { ") - $$$GENERATE(indent_" Set first=value.Read(.len,.sc) If $$$ISERR(sc) Goto "_%exitCode) - $$$GENERATE(indent_" For k=1:1:$length(first) {"_%outputCode_"$select($ascii(first,k)<16:""0"",1:"""")_$zhex($ascii(first,k)) }") - $$$GENERATE(indent_" }} "_%outputCode_"""""""""") + $$$GENERATE(indent_"Do value.Rewind()") + $$$GENERATE(indent_"Set hexString=""""") + $$$GENERATE(indent_"If value.Size>0 { While 'value.AtEnd { ") + $$$GENERATE(indent_" Set first=value.Read(.len,.sc) $$$ThrowOnError(sc)") + $$$GENERATE(indent_" For k=1:1:$length(first) {") + $$$GENERATE(indent_" Set hexString=hexString_$select($ascii(first,k)<16:""0"",1:"""")_$zhex($ascii(first,k))") + $$$GENERATE(indent_" }") + $$$GENERATE(indent_"}}") + Do ..GenSetField(fieldName,indent,"hexString","""string""") } Else { - $$$GENERATE(indent_" "_%outputCode_""""""""" Do value.Rewind() If value.Size>0 { While 'value.AtEnd { "_%outputCode_"$system.Encryption.Base64Encode(value.Read(,.sc),1) If $$$ISERR(sc) Goto "_%exitCode_" }} "_%outputCode_"""""""""") + Do ..GenSetField(fieldName,indent,"value","""stream>base64""") } } /// Generate code for exporting a character stream as JSON. ClassMethod GenExportCharacterStream(class As %String, propertyMap As %List, fieldName As %String, indent As %String, isCollectionValue As %Integer) [ Internal ] { - Do ..GenWriteField(fieldName,indent,"") - $$$GENERATE(indent_" "_%outputCode_""""""""" Do value.Rewind() If value.Size>0 { While 'value.AtEnd { "_%outputCode_"$zcvt(value.Read(,.sc),""O"",""JSON"") If $$$ISERR(sc) Goto "_%exitCode_" }} "_%outputCode_"""""""""") + Do ..GenSetField(fieldName,indent,"value","""stream""") } /// Generate code for exporting a literal value as JSON. @@ -1547,35 +1755,50 @@ ClassMethod GenExportLiteral(class As %String, propertyMap As %List, fieldName A } If 'haveLogicalToJSON Set codeLogicalToJSON="value" Set literaltype=$$$jsonliteraltype(propertyMap) - If $piece(literaltype,"/",1)="string" { - If literaltype="string" { - Set codeLogicalToJSON="$select("_$select($$$jsonignorenull(propertyMap):"(value="""""""")||",1:"")_"(value=$c(0)):"""",1:$zcvt("_codeLogicalToJSON_",""O"",""JSON""))" + If (literaltype="runtime") && 'haveLogicalToJSON { + $$$ThrowStatus($$$ERROR($$$GeneralError,$$$FormatText("With JSONTYPE of runtime, LogicalToJSON() method MUST be defined for data type %1 on property '%2'",type,property))) + } + + // Check if literaltype is one of the known JSON types that can be used with %Set. + // If so, then use it. + // If not, it is invalid for the %Set command. Let type get figured out by IRIS. + Set jsontype = "" + Set validJsonTypes = "null,boolean,number,string,stringbase64,stream,stream>base64,stream%pkg.isc.json.mapping. +Class %pkg.isc.json.mappingInfo Extends %RegisteredObject +{ + +/// Name of class for which mapping is returned. +Property Classname As %Dictionary.Classname; + +/// Name of mapping. Will be empty for default mapping. +Property Mapping As %String; + +/// Whether invalid fields are ignored. +/// Default matches default value of %JSONIGNOREINVALIDFIELD in %pkg.isc.json.adaptor. +Property IgnoreInvalidField As %Boolean [ InitialExpression = 0 ]; + +/// List of properties present in the mapping. +Property Properties As list Of %pkg.isc.json.mappingInfo.property; + +/// Whether to allow the ID to be included (output-only) in representations of the object +Property IncludeID As %Boolean; + +/// The field name to use for the ID in representations of the object, if enabled by %JSONINCLUDEID +Property IDField As %String; + +} + diff --git a/cls/_pkg/isc/json/mappingInfo/property.cls b/cls/_pkg/isc/json/mappingInfo/property.cls new file mode 100644 index 0000000..f8a3d89 --- /dev/null +++ b/cls/_pkg/isc/json/mappingInfo/property.cls @@ -0,0 +1,43 @@ +/// Class representation of a single property of a JSON mapping that has been +/// processed during compilation. +Class %pkg.isc.json.mappingInfo.property Extends %pkg.isc.json.mappingProperty +{ + +/// Normalized name of the property in the source class. +Property NormalizedName As %Dictionary.Identifier; + +/// If the property is a literal, the specific JSON type of the literal. +Property LiteralType As %String; + +/// ObjectScript representation of the member category that the property falls into. +/// Available options are enumerated in the GetMemberInfo() method of +/// %pkg.isc.json.generator. +Property MemberCategory As %String; + +/// ObjectScript type of the property. +Property Type As %Dictionary.Classname; + +Method %OnNew(name As %String, normalizedName As %String, include As %String, fieldName As %String, null As %Boolean, ignoreNull As %Boolean, reference As %String, required As %Boolean, literalType As %String, memberCategory As %String, type As %String, mapping As %String) As %Status [ Private, ServerOnly = 1 ] +{ + Set sc = $$$OK + Try { + Set ..Name = name + Set ..NormalizedName = normalizedName + Set ..Include = include + Set ..FieldName = fieldName + Set ..Null = null + Set ..IgnoreNull = ignoreNull + Set ..Reference = reference + Set ..Required = required + Set ..LiteralType = literalType + Set ..MemberCategory = memberCategory + Set ..Type = type + Set ..Mapping = mapping + } Catch (ex) { + Set sc = ex.AsStatus() + } + Return sc +} + +} + diff --git a/cls/_pkg/isc/json/mappingProperty.cls b/cls/_pkg/isc/json/mappingProperty.cls index 240f266..d009298 100644 --- a/cls/_pkg/isc/json/mappingProperty.cls +++ b/cls/_pkg/isc/json/mappingProperty.cls @@ -35,5 +35,8 @@ Property Reference As %String(XMLPROJECTION = "attribute"); /// See %JSONREQUIRED property parameter in %pkg.isc.json.propertyParameters Property Required As %Boolean(XMLPROJECTION = "attribute"); +/// See %JSONTYPE property parameter in %pkg.isc.json.propertyParameters +Property JsonType As %String(XMLPROJECTION = "attribute"); + } diff --git a/cls/_pkg/isc/json/propertyParameters.cls b/cls/_pkg/isc/json/propertyParameters.cls index ccdab92..229f1f6 100644 --- a/cls/_pkg/isc/json/propertyParameters.cls +++ b/cls/_pkg/isc/json/propertyParameters.cls @@ -42,5 +42,10 @@ Parameter %JSONMAPPING As STRING; /// If unspecified, the same behavior is determined based on the [ Required] keyword. Parameter %JSONREQUIRED As BOOLEAN; +/// Override default JSONTYPE parameter for literal property. Has no effect on non-literal properties. +/// This must be a valid JSONTYPE i.e. one of the possibly return values of +/// %DynamicAbstractObject method %GetTypeOf(). +Parameter %JSONTYPE As STRING; + } diff --git a/docs/user-guide.md b/docs/user-guide.md index 9bf1dde..cdaa477 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -19,12 +19,25 @@ The first place to start on understanding isc.json is to read up on the %JSON pa ## Overview: What's new in isc.json Features: -* Support for %List datatype projection to/from arrays -* Ability to easily include row IDs in JSON projection of persistent classes -* "Studio Assist" schema for XData blocks -* Ability to layer/extend mappings within a given class -* Ability to replace collections rather than append on %JSONImport -* Support for simple PascalCase->camelCase conversion by convention +* Support for %List datatype projection to/from arrays. +* Ability to easily include row IDs in JSON projection of persistent classes. +* Ability to layer/extend mappings within a given class. +* Support for simple PascalCase->camelCase conversion by convention. +* %JSONMappingInfo is a new generated method that provides metadata of JSON projections (XData blocks +and the base mapping). +* Data type class can have a new JSONTYPE of runtime instead of a static value. +* %JSONNew provides a generated implementation in %JSONNewDefault for persistent classes to match +against provided ID or fields that are used for unique indices. +* New method %JSONExportToDynamicObject to get complete closure of easily doing export/import directly +to/from %DYnamicObject. +* New methods %JSONExportArray and %JSONImportArray to do bulk import/export. + +Enhancements: +* "Studio Assist" schema for XData blocks. +* Ability to replace collections rather than append on %JSONImport. +* Support for import/export of properties that are %DynamicObject/%DynamicArray. +* JSON export is done to %DynamicObject which is then exported to string/stream/current device as +needed which provides better performance and more gracefully handles `` errors. Bug fixes: * Non-JSON-related XData blocks don't make class compilation fail @@ -241,5 +254,111 @@ ClassMethod Demo() } ``` +### %JSONMappingInfo + +If you want to build any tooling that relies on the JSON projection of a class or just want to see the +metadata for the projections as an ObjectScript object, it is made available via this generated method. +The method returns an instance of [`%pkg.isc.json.mappingInfo`](../cls/pkg/isc/json/mappingInfo.cls) with +top level info about the provided mapping (provide empty string to get info about the base mapping). + +Example: +``` +Class isc.sample.json.mappingInfo Extends (%RegisteredObject, %pkg.isc.json.adaptor) +{ + +Property FirstName As %String; + +Property LastName As %String; + +XData FirstNameOnly [ XMLNamespace = "http://www.intersystems.com/_pkg/isc/json/jsonmapping" ] +{ + + + +} + +ClassMethod Demo() +{ + Set baseInfo = ..%JSONMappingInfo() + // Output: baseInfo.Properties will contain FirstName and LastName. + Set firstNameOnlyInfo = ..%JSONMappingInfo("FirstNameOnly") + // Output: baseInfo.Properties will contain only FirstName and baseInfo.IncludeID will be 1. +} + +} +``` + +### Data types with JSONTYPE as runtime + +This is a somewhat niche feature and should not generally be needed. With this feature, a data type +class when created can be given a parameter of JSONTYPE set to runtime so that on export/import, based on the value, the output can dynamically have different JSON types (e.g. number or string). + +When this is done, the following methods MUST be implemented in the data type class: +``` +/// Returns a valid JSON type. See methods of %DynamicAbstracyObject for valid JSON types. +ClassMethod GetJSONTYPE(value As %String) As %String +{ +} + +/// Returns whether the value is valid for the given type +ClassMethod IsValid(value As %String) As %Status +{ +} + +/// Convert from JSON to ObjectScript logical value (used in JSON import) +ClassMethod JSONToLogical(value As %String) As %String +{ + Return value +} + +/// Convert from ObjectScript logical value to JSON (used in JSON export) +ClassMethod LogicalToJSON(value As %String) As %String +{ + If ##class(%Integer).IsValid(value) { + Return value + } + Return $$$QUOTE(value) +} +``` + +An example can be seen in [`UnitTest.isc.json.sample.intOrString`](../internal/testing/unit_tests/UnitTest/isc/json/sample/intOrString.cls). + +### %JSONNew generated implementation for %Persistent classes + +The `%JSONNew` method is used to generate a record of a class extending `%pkg.isc.json.adaptor`. It is +used when `%JSONImport` needs to be recusrively called for a property that is of a type that extends `%pkg.isc.json.adaptor`. `%JSONImport` is an instance method and so requires an object for it to be called on. +`%JSONNew` provides this object. + +The changes made to the `%JSONNew` method are the following: +- New parameter mappingName is passed in to give the context of the JSON mapping in use during import. + - For backwards compatability, legacy `%JSONNew` without the third argument will also work. +- `%JSONNew` will by default be a wrapper around a new method `%JSONNewDefault`. +- `%JSONNewDefault` is generated as follows: + - For a non-persistent class: calls %New() on the class and returns the object. + - For a persistent class: matches against any unique indices and row IDs in the JSON input to identify + an existing record to open with `%OpenId`. If an existing record is not found, then a new record + is created with `%New()` and returned. If properties to construct the unique index or the row ID is + missing from the JSON input (either if not required or not in the JSON mapping), then corresponding + attempts to match are not made. When matching, the following precedence is in effect: + - row ID + - IdKey index + - Unique indices in reverse alphabetical order (later in alphabet gets higher precedence than earlier) + +See examples in the generated code of [`UnitTest.isc.json.sample.jsonNew`](../internal/testing/unit_tests/UnitTest/isc/json/sample/jsonNew.cls) and [`UnitTest.isc.json.sample.jsonNewPersistent`](../internal/testing/unit_tests/UnitTest/isc/json/sample/jsonNewPersistent.cls). + +### %JSONExportToDynamicObject + +This is simply a new method following a similar format to other %JSONExport* methods. + +### %JSONImportArray and %JSONExportArray + +These methods allow for bulk import and export of %DynamicArray. Read method documentation for detailed behavior. + +To summarize: +- %JSONImport: Allows accumulating errors or throwing on the first error as well as saving while +importing for persistent records. +- %JSONExport: Allows accumulating errors or throwing on the first error as well as a variety of source +for export such as %List, %ListOfObjects, %ListOfDataTypes etc. Up-to-date sources are noted in the method documentation. + ## Related Topics in InterSystems Documentation * [Using the JSON Adaptor](https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=GJSON_adaptor) diff --git a/inc/pkg/isc/json/map.inc b/inc/pkg/isc/json/map.inc new file mode 100644 index 0000000..8a33390 --- /dev/null +++ b/inc/pkg/isc/json/map.inc @@ -0,0 +1,45 @@ +ROUTINE %pkg.isc.json.map [Type=INC] +#;-----------------Stolen from %jsonMap----------------- +#;Macros for JSON map +#define jsonclass(%map,%name) $listget(%map(%name,0),1) +#define jsonignoreinvalidfield(%map,%name) $listget(%map(%name,0),2) +#define jsonincludeid(%map,%name) $listget(%map(%name,0),3) +#define jsonidfield(%map,%name) $listget(%map(%name,0),4) +#define jsonpropertycount(%map,%name) $get(%map(%name)) +#define jsongetprop(%map,%name,%idx) $get(%map(%name,%idx)) +#define jsonproperty(%node) $list(%node,1) +#define jsonpropertyQN(%node) $list(%node,2) +#; Possible values of jsoninclude are "N","I","O","IO" +#define jsoninclude(%node) $list(%node,3) +#define jsonfieldname(%node) $list(%node,4) +#define jsonnull(%node) $list(%node,5) +#define jsonignorenull(%node) $list(%node,6) +#; Possible jsonreference values +#define jsonrefobject 1 +#define jsonrefid 2 +#define jsonrefoid 3 +#define jsonrefguid 4 +#define jsonreference(%node) $list(%node,7) +#define jsonrequired(%node) $list(%node,8) +#define jsonliteraltype(%node) $list(%node,9) +#define jsonmembercat(%node) $list(%node,10) +#define jsontype(%node) $list(%node,11) +#define jsonmapping(%node) $list(%node,12) +#; +#; isCollectionValue possibilities +#define isCollectionList 1 +#define isCollectionArray 2 +#; External display format for mapping name +#define mappingNameDisplay(%name) $select(%name="":"class base",1:%name) +#;------------Helpers for %pkg.isc.json.path-------------- +#define jsonIsValidArraySubscript(%val) ((%val = (%val\1)) && (%val >= 0)) +#define jsonKeyDefined(%obj,%key) (%obj.%IsDefined(%key) && ('%obj.%IsA("%Library.DynamicArray") || $$$jsonIsValidArraySubscript(%key))) +#;-----------------Error macro stubs----------------- +#;----------Custom errors added in %pkg.isc.*------------- +#define JSONReferencedTypeIsNotAdapted 9450 +#; JSONInvalidIDPROJECTION is similar to %ObjectErrors 6259 (XMLInvalidIDPROJECTION) +#define JSONInvalidIDPROJECTION 9451 +#define JSONInvalidMapping 9452 +#define JSONInvalidListForExportArray 9453 +#define JSONPathExpressionError 9460 +#define JSONPathEvalError 9461 diff --git a/internal/testing/unit_tests/UnitTest/isc/json/collections.cls b/internal/testing/unit_tests/UnitTest/isc/json/collections.cls index c64a0d6..a6963b2 100644 --- a/internal/testing/unit_tests/UnitTest/isc/json/collections.cls +++ b/internal/testing/unit_tests/UnitTest/isc/json/collections.cls @@ -52,6 +52,16 @@ Method TestArrayWithTwoValues() Do $$$AssertEquals(obj.Map.GetAt("RI"),"Providence") } +Method TestArrayWithEscapedProperty() +{ + Set exportObj = ..GetTestObject() + Set importObj = ##class(UnitTest.isc.json.collectionExample).%New() + Do exportObj.Map.SetAt("Desert Mouse","Muad""Dib") + Do $$$AssertStatusOK(exportObj.%JSONExportToString(.tJSON)) + Do $$$AssertStatusOK(importObj.%JSONImport(tJSON)) + Do $$$AssertEquals(importObj.Map.GetAt("Muad""Dib"),"Desert Mouse") +} + Method GetTestObject() As UnitTest.isc.json.collectionExample { Set obj = ##class(UnitTest.isc.json.collectionExample).%New() diff --git a/internal/testing/unit_tests/UnitTest/isc/json/dataTypes.cls b/internal/testing/unit_tests/UnitTest/isc/json/dataTypes.cls index 6ad82bf..5bfc72a 100644 --- a/internal/testing/unit_tests/UnitTest/isc/json/dataTypes.cls +++ b/internal/testing/unit_tests/UnitTest/isc/json/dataTypes.cls @@ -19,6 +19,33 @@ Property ArrayList As %pkg.isc.json.dataType.list [ InitialExpression = {$ListBu Property ReferenceObject As %DynamicObject(%JSONINCLUDE = "NONE"); +Property JObject As %DynamicObject; + +Property JArray As %DynamicArray; + +Property JAnyAsObject As %DynamicAbstractObject; + +Property JAnyAsArray As %DynamicAbstractObject; + +Property IntOrString As UnitTest.isc.json.sample.intOrString; + +XData IntOrStringOnly [ XMLNamespace = "http://www.intersystems.com/_pkg/isc/json/jsonmapping" ] +{ + + + +} + +Method %OnNew(initvalue) As %Status +{ + set sc = ##super(initvalue) + set ..JObject = { "a":1 } + set ..JArray = [ 1,2,3 ] + set ..JAnyAsObject = { "a":2 } + set ..JAnyAsArray = [ 10,20,30] + Quit sc +} + Method TestAAAALogJSON() { // Named so that it runs first @@ -92,15 +119,97 @@ Method TestEndtoEnd() Method TestCompilingMe() { // Gets Generator test coverage credit! - Do $$$AssertStatusOK($System.OBJ.Compile($classname(),"ck/nomulticompile")) + Do $$$AssertStatusOK($System.OBJ.Compile($classname(),"ck/display=none/nomulticompile")) } Method TestLogicalToJSONDirect() { // Gets direct test coverage credit on %pkg.isc.json.dataType.list - Do $$$AssertEquals(##class(%pkg.isc.json.dataType.list).LogicalToJSON($lb(1,,2,3,)),"[1,null,2,3,null]") + Do $$$AssertEquals(##class(%pkg.isc.json.dataType.list).LogicalToJSON($lb(1,,2,3,)).%ToJSON(),"[1,null,2,3,null]") Do $$$AssertEquals(##class(%pkg.isc.json.dataType.list).JSONToLogical([1,null,2,3,null]),$lb(1,,2,3,)) } +Method TestDynamicObject() +{ + Do $$$AssertEquals(..ReferenceObject.%GetTypeOf("JObject"),"object") + Do $$$AssertEquals(..ReferenceObject.JObject.%ToJSON(),..JObject.%ToJSON()) +} + +Method TestDynamicArray() +{ + Do $$$AssertEquals(..ReferenceObject.%GetTypeOf("JArray"),"array") + Do $$$AssertEquals(..ReferenceObject.JArray.%ToJSON(),..JArray.%ToJSON()) +} + +Method TestAnyAsObject() +{ + Do $$$AssertEquals(..ReferenceObject.%GetTypeOf("JAnyAsObject"),"object") + Do $$$AssertEquals(..ReferenceObject.JAnyAsObject.%ToJSON(),..JAnyAsObject.%ToJSON()) } +Method TestAnyAsArray() +{ + Do $$$AssertEquals(..ReferenceObject.%GetTypeOf("JAnyAsArray"),"array") + Do $$$AssertEquals(..ReferenceObject.JAnyAsArray.%ToJSON(),..JAnyAsArray.%ToJSON()) +} + +Method TestDynamicArrayAsObject() +{ + // Set an object into an array property + set newobj = ..%New($$$NULLOREF) + Set newobj.JArray = { "a":10 } + Do newobj.%JSONExportToString(.json) + set sc = newobj.%JSONImport( json ) + if $$$ISOK(sc) { + Do $$$AssertFailure("Expected Error when importing %DynamicObject into %DynamicArray property") + } else { + Do $$$AssertSuccess("Got Error when importing %DynamicObject into %DynamicArray property") + } +} + +Method TestDynamicObjectAsArray() +{ + // Set an array into an object property + Set newobj = ..%New($$$NULLOREF) + Set newobj.JObject = [ "a","b"] + Do newobj.%JSONExportToString(.json) + set sc = newobj.%JSONImport( json ) + if $$$ISOK(sc) { + Do $$$AssertFailure("Expected Error when importing %DynamicArray into %DynamicObject property") + } else { + Do $$$AssertSuccess("Got Error when importing %DynamicArray into %DynamicObject property") + } +} + +Method TestRuntimeIntOrString() +{ + // String + Set json = { "IntOrString": "string" } + Set o = ..%New($$$NULLOREF) + Set sc = o.%JSONImport(json) + Do $$$AssertStatusOK(sc) + Do $$$AssertEquals(o.IntOrString,"string") + Kill str + Set sc = o.%JSONExportToString(.str,"IntOrStringOnly") + Do $$$AssertStatusOK(sc) + Do $$$AssertEquals(str,json.%ToJSON()) + + // Integer + Set json = { "IntOrString": 1234 } + Set o = ..%New($$$NULLOREF) + Set sc = o.%JSONImport(json) + Do $$$AssertStatusOK(sc) + Do $$$AssertEquals(o.IntOrString,1234) + Kill str + Set sc = o.%JSONExportToString(.str,"IntOrStringOnly") + Do $$$AssertStatusOK(sc) + Do $$$AssertEquals(str,json.%ToJSON()) + + // Boolean - should fail + Set json = { "IntOrString": true } + Set o = ..%New($$$NULLOREF) + Set sc = o.%JSONImport(json) + Do $$$AssertStatusNotOK(sc) +} + +} diff --git a/internal/testing/unit_tests/UnitTest/isc/json/export.cls b/internal/testing/unit_tests/UnitTest/isc/json/export.cls new file mode 100644 index 0000000..f34dc8e --- /dev/null +++ b/internal/testing/unit_tests/UnitTest/isc/json/export.cls @@ -0,0 +1,429 @@ +Include %pkg.isc.json.map + +/// Tests for %JSONExport/ToString/ToStream/ToDynamicObject methods +/// zpm "isc.json test -DUnitTest.Case=UnitTest.isc.json.export" +Class UnitTest.isc.json.export Extends UnitTest.isc.json.testBase +{ + +/// Spec: Export persistent object to DynamicObject +Method TestExportToDynamicObjectPersistent() +{ + Set id = ..CreateSavedPersistent("E1", 31) + Set o = ##class(UnitTest.isc.json.sample.generalPersistent).%OpenId(id) + Set sc = o.%JSONExportToDynamicObject(.json, "") + Do $$$AssertStatusOK(sc) + Do $$$AssertTrue($IsObject(json)) + // Scalars + Do $$$AssertEquals(json.%Get("Name"), o.Name) + Do $$$AssertEquals(json.%Get("Age"), o.Age) + Do $$$AssertEquals(json.%Get("DoubleVal"), o.DoubleVal) + Do $$$AssertEquals(json.%Get("Active"), o.Active) + Do $$$AssertEquals(json.%Get("NameIgnoreNull"), o.NameIgnoreNull) + Do $$$AssertEquals(json.%Get("Snake_Case"), o."Snake_Case") + Do $$$AssertEquals(json.%Get("Timestamp"), ##class(%TimeStamp).LogicalToXSD(o.Timestamp)) + Do $$$AssertEquals(json.%Get("IntOrString"), o.IntOrString) + + // JSON types + Do $$$AssertTrue($IsObject(json.%Get("jsonObj"))) + Do $$$AssertEquals(json.%Get("jsonObj").%Get("k"), o.jsonObj.%Get("k")) + Do $$$AssertTrue($IsObject(json.%Get("jsonArr"))) + Do $$$AssertEquals(json.%Get("jsonArr").%Size(), o.jsonArr.%Size()) + Do $$$AssertEquals(json.%Get("jsonArr").%Get(0), o.jsonArr.%Get(0)) + Do $$$AssertEquals(json.%Get("jsonArr").%Get(1), o.jsonArr.%Get(1)) + + // Streams + Do $$$AssertEquals(json.%Get("CharStream"), "chars-"_o.Name) + Do $$$AssertEquals(json.%Get("BinaryStream"), "AQIDBA==") + Do $$$AssertEquals(json.%Get("BinaryStreamHex"), "ABCDEF") + + // Nested serial object + Set si = json.%Get("SubItem") + Do $$$AssertTrue($IsObject(si)) + Do $$$AssertEquals(si.%Get("Code"), "E1_C") + Do $$$AssertEquals(si.%Get("Value"), 31) + + // Referenced persistent object (OBJECT) + Set spi = json.%Get("SubItemPersistent") + Do $$$AssertTrue($IsObject(spi)) + Do $$$AssertEquals(spi.%Get("Code"), "E1_PC") + Do $$$AssertEquals(spi.%Get("Value"), 32) + + // Reference projections + Do $$$AssertEquals(json.%Get("SubItemPersistentID"), o.SubItemPersistent.%Id()) + Set expectedOID = $listget(o.SubItemPersistent.%Oid(),2)_","_$listget(o.SubItemPersistent.%Oid()) + Do $$$AssertEquals(json.%Get("SubItemPersistentOID"), expectedOID) + Set expectedGUID = o.SubItemPersistent.%GUID(o.SubItemPersistent.%Oid()) + Do $$$AssertEquals(json.%Get("SubItemPersistentGUID"), expectedGUID) + + // Collections + Set tags = json.%Get("Tags") + Do $$$AssertTrue($IsObject(tags)) + Do $$$AssertEquals(tags.%Size(), 2) + Do $$$AssertEquals(tags.%Get(0), "E1_t1") + Do $$$AssertEquals(tags.%Get(1), "E1_t2") + + Set subl = json.%Get("SubList") + Do $$$AssertTrue($IsObject(subl)) + Do $$$AssertEquals(subl.%Size(), 1) + Do $$$AssertEquals(subl.%Get(0).%Get("Code"), "E1_SL") + Do $$$AssertEquals(subl.%Get(0).%Get("Value"), 33) + + Set attr = json.%Get("Attr") + Do $$$AssertTrue($IsObject(attr)) + Do $$$AssertEquals(attr.%Get("key1"), "E1_val1") + Do $$$AssertEquals(attr.%Get("key2"), "E1_val2") + + Set items = json.%Get("Items") + Do $$$AssertTrue($IsObject(items)) + Do $$$AssertTrue($IsObject(items.%Get("first"))) + Do $$$AssertEquals(items.%Get("first").%Get("Code"), "E1_IT") + Do $$$AssertEquals(items.%Get("first").%Get("Value"), 34) + + // Relationships not set should be empty string, not arrays + Do $$$AssertEquals(json.%Get("Children"), "") + Do $$$AssertEquals(json.%Get("Relateds"), "") +} + +/// Spec: Unset collections export as "" and not [] / {} +Method TestExportToDynamicObjectCollectionsUnsetNotExported() +{ + Set o = ..BuildUnsavedPersistent("C0", 20) + Do o.Tags.Clear() + Do o.Children.Clear() + Set sc = o.%JSONExportToDynamicObject(.json, "") + Do $$$AssertStatusOK(sc) + Do $$$AssertTrue($IsObject(json)) + + // Lists + Do $$$AssertTrue('json.%IsDefined("Tags")) + Do $$$AssertTrue('json.%IsDefined("SubList")) + + // Relationships + Do $$$AssertTrue('json.%IsDefined("Children")) + Do $$$AssertTrue('json.%IsDefined("Relateds")) +} + +/// Spec: Uses NameOnly mapping to include only Name with field rename +Method TestExportToDynamicObjectWithNameOnlyMapping() +{ + Set id = ..CreateSavedPersistent("Map1", 40) + Set o = ##class(UnitTest.isc.json.sample.generalPersistent).%OpenId(id) + Set sc = o.%JSONExportToDynamicObject(.json, "NameOnly") + Do $$$AssertStatusOK(sc) + Do $$$AssertTrue($IsObject(json)) + Do $$$AssertEquals(json.%Get("name"), "Map1") + Do $$$AssertTrue('json.%IsDefined("Age")) +} + +/// Spec: Export non-persistent object to DynamicObject +Method TestExportToDynamicObjectNonPersistent() +{ + Set o = ..BuildUnsavedNonPersistent("N1") + Set sc = o.%JSONExportToDynamicObject(.json, "") + Do $$$AssertStatusOK(sc) + Do $$$AssertEquals(json.%Get("Name"), "N1") +} + +/// Spec: Export persistent object to string JSON +Method TestExportToStringPersistent() +{ + Set o = ..BuildUnsavedPersistent("S1", 28) + Set sc = o.%JSONExportToString(.str) + Do $$$AssertStatusOK(sc) + Do $$$AssertTrue($Length(str)>0) + Set dobj = {}.%FromJSON(str) + Do $$$AssertEquals(dobj.%Get("Name"), "S1") +} + +/// Spec: NameAge mapping exports only Name and Age with lowercase field names +Method TestExportToStringWithNameAgeMapping() +{ + Set o = ..BuildUnsavedPersistent("S2", 33) + Set sc = o.%JSONExportToString(.s, "NameAge") + Do $$$AssertStatusOK(sc) + Set dobj = {}.%FromJSON(s) + Do $$$AssertEquals(dobj.%Get("name"), "S2") + Do $$$AssertEquals(dobj.%Get("age"), 33) + Do $$$AssertTrue('dobj.%IsDefined("Active")) +} + +/// Spec: Export persistent object to provided stream +Method TestExportToStreamPersistent() +{ + Set o = ..BuildUnsavedPersistent("T1", 29) + Set stream = ##class(%Stream.GlobalCharacter).%New() + Set sc = o.%JSONExportToStream(.stream) + Do $$$AssertStatusOK(sc) + Do stream.Rewind() + Set dobj = {}.%FromJSON(stream) + Do $$$AssertEquals(dobj.%Get("Name"), "T1") +} + +/// Spec: NoStreams mapping excludes stream properties from export +Method TestExportToStreamWithNoStreamsMapping() +{ + Set o = ..BuildUnsavedPersistent("TS", 35) + Set stream = ##class(%Stream.GlobalCharacter).%New() + Set sc = o.%JSONExportToStream(.stream, "NoStreams") + Do $$$AssertStatusOK(sc) + Do stream.Rewind() + Set dobj = {}.%FromJSON(stream) + Do $$$AssertEquals(dobj.%Get("CharStream"), "") + Do $$$AssertEquals(dobj.%Get("BinaryStream"), "") + Do $$$AssertEquals(dobj.%Get("BinaryStreamHex"), "") +} + +/// Spec: Export to current device returns success +Method TestExportToCurrentDevice() +{ + Set o = ..BuildUnsavedPersistent("D1", 27) + // We do not capture output; ensure it succeeds + Set sc = o.%JSONExport() + Do $$$AssertStatusOK(sc) +} + +/// Spec: Invalid mapping name is rejected in DynamicObject export +Method TestExportToDynamicObjectInvalidMapping() +{ + Set o = ..BuildUnsavedPersistent("MX", 26) + Set sc = o.%JSONExportToDynamicObject(.json, "AltMap") + Do $$$AssertStatusNotOK(sc) + Do $$$AssertEquals($System.Status.GetErrorCodes(sc),$$$JSONInvalidMapping) +} + +/// Spec: Invalid mapping name is rejected in string export +Method TestExportToStringInvalidMapping() +{ + Set o = ..BuildUnsavedPersistent("MS", 26) + Set sc = o.%JSONExportToString(.str, "AltMap") + Do $$$AssertStatusNotOK(sc) + Do $$$AssertEquals($System.Status.GetErrorCodes(sc),$$$JSONInvalidMapping) +} + +/// Spec: Invalid mapping name is rejected in stream export +Method TestExportToStreamInvalidMapping() +{ + Set o = ..BuildUnsavedPersistent("MT", 26) + Set s = ##class(%Stream.GlobalCharacter).%New() + Set sc = o.%JSONExportToStream(.s, "AltMap") + Do $$$AssertStatusNotOK(sc) + Do $$$AssertEquals($System.Status.GetErrorCodes(sc),$$$JSONInvalidMapping) +} + +/// Spec: Invalid mapping name is rejected in device export +Method TestExportInvalidMapping() +{ + Set o = ..BuildUnsavedPersistent("MD", 26) + Set sc = o.%JSONExport("AltMap") + Do $$$AssertStatusNotOK(sc) + Do $$$AssertEquals($System.Status.GetErrorCodes(sc),$$$JSONInvalidMapping) +} + +/// Spec: Simple mapping applies same-named child mapping to SubItem +Method TestExportToDynamicObjectWithSimpleMapping() +{ + Set id = ..CreateSavedPersistent("PX1", 41) + Set o = ##class(UnitTest.isc.json.sample.generalPersistent).%OpenId(id) + Set sc = o.%JSONExportToDynamicObject(.json, "Simple") + Do $$$AssertStatusOK(sc) + Do $$$AssertEquals(json.%Get("name"), o.Name) + Set si = json.%Get("SubItem") + Do $$$AssertTrue($IsObject(si)) + Do $$$AssertEquals(si.%Get("code"), o.SubItem.Code) + Do $$$AssertEquals(si.%Get("value"), o.SubItem.Value) + // Ensure original-cased fields are not present + Do $$$AssertTrue('si.%IsDefined("Code")) + Do $$$AssertTrue('si.%IsDefined("Value")) +} + +/// Spec: ParentChildAlt mapping uses child's CodeOnly mapping (code only, no value) +Method TestExportToDynamicObjectWithParentChildAlt() +{ + Set id = ..CreateSavedPersistent("PX2", 42) + Set o = ##class(UnitTest.isc.json.sample.generalPersistent).%OpenId(id) + Set sc = o.%JSONExportToDynamicObject(.json, "ParentChildAlt") + Do $$$AssertStatusOK(sc) + Do $$$AssertEquals(json.%Get("Name"), o.Name) + Set si = json.%Get("SubItem") + Do $$$AssertTrue($IsObject(si)) + Do $$$AssertEquals(si.%Get("code"), o.SubItem.Code) + Do $$$AssertTrue('si.%IsDefined("value")) +} + +/// Spec: Invalid child mapping referenced in mapping causes export failure +Method TestExportToDynamicObjectInvalidChildMapping() +{ + Set id = ..CreateSavedPersistent("PX3", 43) + Set o = ##class(UnitTest.isc.json.sample.generalPersistent).%OpenId(id) + Set sc = o.%JSONExportToDynamicObject(.json, "InvalidChildMap") + Do $$$AssertStatusNotOK(sc) + Do $$$AssertEquals($System.Status.GetErrorCodes(sc),$$$JSONInvalidMapping) +} + +/// Spec: Ensure elements which should be escaped are indeed escaped +Method TestExportToDynamicObjectCharEscaping() +{ + Set escapeString = "Hello"_$Char(9)_"""today\/"_$Char(13,10) + Set id = ..CreateSavedPersistent(escapeString) + Set o = ##class(UnitTest.isc.json.sample.generalPersistent).%OpenId(id) + Set sc = o.%JSONExportToDynamicObject(.json) + Do $$$AssertStatusOK(sc) + Do $$$AssertEquals(json.%Get("Name"),escapeString) +} + +/// Spec: Empty SubItem and SubItem collections are omitted from output +Method TestExportOmitEmptySubItems() +{ + Set o = ..BuildUnsavedPersistent("ESI", 22) + // Ensure nothing is populated for SubItem/Items/SubList/Tags/Attr + Do o.Tags.Clear() + Do o.SubList.Clear() + Do o.SubListPersistent.Clear() + Do o.Items.Clear() + Do o.Attr.Clear() + Do o.ItemsPersistent.Clear() + // Clear array properties by leaving them unset (default) for this unsaved object + Set sc = o.%JSONExportToDynamicObject(.json, "") + Do $$$AssertStatusOK(sc) + // None of the collections should be defined + Do $$$AssertTrue('json.%IsDefined("Tags")) + Do $$$AssertTrue('json.%IsDefined("SubList")) + Do $$$AssertTrue('json.%IsDefined("SubListPersistent")) + Do $$$AssertTrue('json.%IsDefined("Items")) + Do $$$AssertTrue('json.%IsDefined("ItemsPersistent")) + Do $$$AssertTrue('json.%IsDefined("Attr")) +} + +/// Spec: Nested empty objects export as null for SubList/Items, but {} for SubListPersistent/ItemsPersistent +Method TestExportOmitEmptyObjectsInCollections() +{ + // Build an unsaved object and populate collections with a mix of empty and non-empty objects + Set o = ..BuildUnsavedPersistent("OC1", 20) + + // SubList (list of objects): insert one empty and one populated serial object + Do o.SubList.Clear() + Set sEmpty = ##class(UnitTest.isc.json.sample.subItem).%New() + Do o.SubList.Insert(sEmpty) + Set sFull = ##class(UnitTest.isc.json.sample.subItem).%New() + Set sFull.Code = "OC1_SL" + Set sFull.Value = 1 + Do o.SubList.Insert(sFull) + + // SubListPersistent (list of persistent objects): one empty and one populated + Do o.SubListPersistent.Clear() + Set spEmpty = ##class(UnitTest.isc.json.sample.subItemPersistent).%New() + Do o.SubListPersistent.Insert(spEmpty) + Set spFull = ##class(UnitTest.isc.json.sample.subItemPersistent).%New() + Set spFull.Code = "OC1_SPL" + Set spFull.Value = 11 + Do o.SubListPersistent.Insert(spFull) + + // Items (array of objects): set an empty entry and a populated entry + Set iEmpty = ##class(UnitTest.isc.json.sample.subItem).%New() + Do o.Items.SetAt(iEmpty, "empty") + Set iFull = ##class(UnitTest.isc.json.sample.subItem).%New() + Set iFull.Code = "OC1_IT" + Set iFull.Value = 2 + Do o.Items.SetAt(iFull, "full") + // ItemsPersistent (array of persistent objects): empty and populated + Set ipEmpty = ##class(UnitTest.isc.json.sample.subItemPersistent).%New() + Do o.ItemsPersistent.SetAt(ipEmpty, "empty") + Set ipFull = ##class(UnitTest.isc.json.sample.subItemPersistent).%New() + Set ipFull.Code = "OC1_IP" + Set ipFull.Value = 12 + Do o.ItemsPersistent.SetAt(ipFull, "full") + + // Export + Set sc = o.%JSONExportToDynamicObject(.json, "") + Do $$$AssertStatusOK(sc) + + // SubList should include both entries: a null and a populated object + Do $$$AssertTrue(json.%IsDefined("SubList")) + Set sl = json.%Get("SubList") + Do $$$AssertTrue($IsObject(sl)) + Do $$$AssertEquals(sl.%Size(), 2) + Do $$$AssertEquals(sl.%GetTypeOf(0), "null") + Do $$$AssertEquals(sl.%Get(1).%Get("Code"), "OC1_SL") + Do $$$AssertEquals(sl.%Get(1).%Get("Value"), 1) + + // SubListPersistent: empty object exported as {} + Do $$$AssertTrue(json.%IsDefined("SubListPersistent")) + Set slp = json.%Get("SubListPersistent") + Do $$$AssertTrue($IsObject(slp)) + Do $$$AssertEquals(slp.%Size(), 2) + Do $$$AssertTrue($IsObject(slp.%Get(0))) + Do $$$AssertEquals(slp.%Get(0).%Size(), 0) + Do $$$AssertEquals(slp.%Get(1).%Get("Code"), "OC1_SPL") + Do $$$AssertEquals(slp.%Get(1).%Get("Value"), 11) + + // Items: empty object exported as null; populated remains + Do $$$AssertTrue(json.%IsDefined("Items")) + Set it = json.%Get("Items") + Do $$$AssertTrue($IsObject(it)) + Do $$$AssertTrue(it.%IsDefined("empty")) + Do $$$AssertEquals(it.%GetTypeOf("empty"), "null") + Do $$$AssertTrue(it.%IsDefined("full")) + Do $$$AssertEquals(it.%Get("full").%Get("Code"), "OC1_IT") + Do $$$AssertEquals(it.%Get("full").%Get("Value"), 2) + + // ItemsPersistent: empty object exported as {} + Do $$$AssertTrue(json.%IsDefined("ItemsPersistent")) + Set ip = json.%Get("ItemsPersistent") + Do $$$AssertTrue($IsObject(ip)) + Do $$$AssertTrue(ip.%IsDefined("empty")) + Do $$$AssertTrue($IsObject(ip.%Get("empty"))) + Do $$$AssertEquals(ip.%Get("empty").%Size(), 0) + Do $$$AssertTrue(ip.%IsDefined("full")) + Do $$$AssertEquals(ip.%Get("full").%Get("Code"), "OC1_IP") + Do $$$AssertEquals(ip.%Get("full").%Get("Value"), 12) +} + +/// Spec: Collections with only empty nested objects still export those elements as {} +Method TestExportOmitCollectionsWhenAllElementsEmpty() +{ + Set o = ..BuildUnsavedPersistent("OC2", 21) + + // SubList contains only an empty object + Do o.SubList.Clear() + Do o.SubList.Insert(##class(UnitTest.isc.json.sample.subItem).%New()) + // SubListPersistent contains only an empty persistent object + Do o.SubListPersistent.Clear() + Do o.SubListPersistent.Insert(##class(UnitTest.isc.json.sample.subItemPersistent).%New()) + + // Items contains only an empty object + Do o.Items.SetAt(##class(UnitTest.isc.json.sample.subItem).%New(), "onlyempty") + // ItemsPersistent contains only an empty persistent object + Do o.ItemsPersistent.SetAt(##class(UnitTest.isc.json.sample.subItemPersistent).%New(), "onlyempty") + + // Export + Set sc = o.%JSONExportToDynamicObject(.json, "") + Do $$$AssertStatusOK(sc) + + // SubList present with one element of null + Do $$$AssertTrue(json.%IsDefined("SubList")) + Do $$$AssertTrue($IsObject(json.%Get("SubList"))) + Do $$$AssertEquals(json.%Get("SubList").%Size(), 1) + Do $$$AssertEquals(json.%Get("SubList").%GetTypeOf(0),"null") + + // SubListPersistent present with one empty object element {} + Do $$$AssertTrue(json.%IsDefined("SubListPersistent")) + Do $$$AssertTrue($IsObject(json.%Get("SubListPersistent"))) + Do $$$AssertEquals(json.%Get("SubListPersistent").%Size(), 1) + Do $$$AssertTrue($IsObject(json.%Get("SubListPersistent").%Get(0))) + Do $$$AssertEquals(json.%Get("SubListPersistent").%Get(0).%Size(), 0) + + // Items present with key mapped to null + Do $$$AssertTrue(json.%IsDefined("Items")) + Do $$$AssertTrue($IsObject(json.%Get("Items"))) + Do $$$AssertTrue(json.%Get("Items").%IsDefined("onlyempty")) + Do $$$AssertEquals(json.%Get("Items").%GetTypeOf("onlyempty"), "null") + + // ItemsPersistent present with key mapped to empty object {} + Do $$$AssertTrue(json.%IsDefined("ItemsPersistent")) + Do $$$AssertTrue($IsObject(json.%Get("ItemsPersistent"))) + Do $$$AssertTrue(json.%Get("ItemsPersistent").%IsDefined("onlyempty")) + Do $$$AssertTrue($IsObject(json.%Get("ItemsPersistent").%Get("onlyempty"))) + Do $$$AssertEquals(json.%Get("ItemsPersistent").%Get("onlyempty").%Size(), 0) +} + +} diff --git a/internal/testing/unit_tests/UnitTest/isc/json/exportArray.cls b/internal/testing/unit_tests/UnitTest/isc/json/exportArray.cls new file mode 100644 index 0000000..d9178b3 --- /dev/null +++ b/internal/testing/unit_tests/UnitTest/isc/json/exportArray.cls @@ -0,0 +1,375 @@ +/// Tests for %JSONExportArray +/// zpm "isc.json test -only -v -DUnitTest.Case=UnitTest.isc.json.exportArray" +Class UnitTest.isc.json.exportArray Extends UnitTest.isc.json.testBase +{ + +Method BuildListOfObjectsPersistent(count As %Integer = 2) As %ListOfObjects +{ + Set list = ##class(%ListOfObjects).%New() + Set list.ElementType = "UnitTest.isc.json.sample.generalPersistent" + For i=1:1:count { + Set o = ##class(UnitTest.isc.json.sample.generalPersistent).%New() + Set o.Name = ("P"_i) + Set o.Age = (20+i) + Do list.Insert(o) + } + Quit list +} + +/// Accepts %ListOfObjects for persistent classes and projects elements +Method TestPersistentListOfObjects() +{ + Set list = ..BuildListOfObjectsPersistent(2) + Kill arr,errorLog + Set sc = ##class(UnitTest.isc.json.sample.generalPersistent).%JSONExportArray(list, "", {}, .arr, .errorLog) + Do $$$AssertStatusOK(sc) + Do $$$AssertTrue($IsObject(arr)) + Do $$$AssertEquals(arr.%Size(), 2) + Do $$$AssertEquals($Data(errorLog),0) + Set o0 = arr.%Get(0) + Set o1 = arr.%Get(1) + Do $$$AssertEquals(o0.%Get("Name"), "P1") + Do $$$AssertEquals(o1.%Get("Age"), 22) +} + +/// Accepts raw $LIST of IDs for persistent classes +Method TestPersistentListOfIDsRawList() +{ + Set id1 = ..CreateSavedPersistent("L1", 31) + Set id2 = ..CreateSavedPersistent("L2", 32) + Set idlist = $Listbuild(id1, id2) + Kill arr,errorLog + Set sc = ##class(UnitTest.isc.json.sample.generalPersistent).%JSONExportArray(idlist, "", {}, .arr, .errorLog) + Do $$$AssertStatusOK(sc) + Do $$$AssertEquals(arr.%Size(), 2) + Do $$$AssertEquals(arr.%Get(0).%Get("Name"), "L1") + Do $$$AssertEquals(arr.%Get(1).%Get("Name"), "L2") + Do $$$AssertEquals($Data(errorLog),0) +} + +/// Accepts %ListOfDataTypes of IDs for persistent classes +Method TestPersistentListOfDataTypesIDs() +{ + Set id1 = ..CreateSavedPersistent("D1", 41) + Set id2 = ..CreateSavedPersistent("D2", 42) + Set lod = ##class(%ListOfDataTypes).%New() + Set lod.ElementType = "%Integer" + Do lod.Insert(id1) + Do lod.Insert(id2) + Kill arr,errorLog + Set sc = ##class(UnitTest.isc.json.sample.generalPersistent).%JSONExportArray(lod, "", {}, .arr, .errorLog) + Do $$$AssertStatusOK(sc) + Do $$$AssertEquals(arr.%Size(), 2) + Do $$$AssertEquals(arr.%Get(0).%Get("Name"), "D1") + Do $$$AssertEquals(arr.%Get(1).%Get("Age"), 42) + Do $$$AssertEquals($Data(errorLog),0) +} + +/// Raw $LIST of IDs with an invalid ID accumulates errors (accumulateErrors=true) +Method TestPersistentRawListErrorAccumulation() +{ + Set id1 = ..CreateSavedPersistent("RL1", 71) + Set badId = 987654321 + Set idlist = $Listbuild(id1, badId) + Kill arr,errorLog + Set sc = ##class(UnitTest.isc.json.sample.generalPersistent).%JSONExportArray(idlist, "", {}, .arr, .errorLog) + Do $$$AssertStatusNotOK(sc) + // One good entry exported, one error logged at index 1 (second element) + Do $$$AssertEquals(arr.%Size(),1) + Do $$$AssertEquals(arr.%Get(0).%Get("Name"), "RL1") + Do $$$AssertTrue($Data(errorLog(1))>0) +} + +/// Raw $LIST of IDs stops at first error when accumulateErrors=false +Method TestPersistentRawListStopOnFirstError() +{ + Set id1 = ..CreateSavedPersistent("RL2", 72) + Set badId = 987654320 + Set id3 = ..CreateSavedPersistent("RL3", 73) + Set idlist = $Listbuild(id1, badId, id3) + Kill arr,errorLog + Set behavior = {"accumulateErrors":0} + Set sc = ##class(UnitTest.isc.json.sample.generalPersistent).%JSONExportArray(idlist, "", behavior, .arr, .errorLog) + Do $$$AssertStatusNotOK(sc) + // Should stop on badId; only first item exported + Do $$$AssertEquals(arr.%Size(),1) + Do $$$AssertEquals(arr.%Get(0).%Get("Name"),"RL2") + // Only one error captured at index 1 (badId) + Set cnt=0,k="" For Set k=$Order(errorLog(k)) Quit:(k="") Set cnt=cnt+1 + Do $$$AssertEquals(cnt,1) + Do $$$AssertTrue($Data(errorLog(1))>0) +} + +/// %ListOfDataTypes with an invalid ID accumulates errors +Method TestPersistentListOfDataTypesErrorAccumulation() +{ + Set id1 = ..CreateSavedPersistent("LD1", 81) + Set badId = 876543210 + Set lod = ##class(%ListOfDataTypes).%New() + Set lod.ElementType = "%Integer" + Do lod.Insert(id1) + Do lod.Insert(badId) + Kill arr,errorLog + Set sc = ##class(UnitTest.isc.json.sample.generalPersistent).%JSONExportArray(lod, "", {}, .arr, .errorLog) + Do $$$AssertStatusNotOK(sc) + Do $$$AssertEquals(arr.%Size(),1) + Do $$$AssertEquals(arr.%Get(0).%Get("Name"), "LD1") + Do $$$AssertTrue($Data(errorLog(1))>0) +} + +/// %ListOfDataTypes stops at first error when accumulateErrors=false +Method TestPersistentListOfDataTypesStopOnFirstError() +{ + Set id1 = ..CreateSavedPersistent("LD2", 82) + Set badId = 876543211 + Set id3 = ..CreateSavedPersistent("LD3", 83) + Set lod = ##class(%ListOfDataTypes).%New() + Set lod.ElementType = "%Integer" + Do lod.Insert(id1) + Do lod.Insert(badId) + Do lod.Insert(id3) + Kill arr,errorLog + Set behavior = {"accumulateErrors":0} + Set sc = ##class(UnitTest.isc.json.sample.generalPersistent).%JSONExportArray(lod, "", behavior, .arr, .errorLog) + Do $$$AssertStatusNotOK(sc) + Do $$$AssertEquals(arr.%Size(),1) + Do $$$AssertEquals(arr.%Get(0).%Get("Name"), "LD2") + Set cnt=0,k="" For Set k=$Order(errorLog(k)) Quit:(k="") Set cnt=cnt+1 + Do $$$AssertEquals(cnt,1) + Do $$$AssertTrue($Data(errorLog(1))>0) +} + +/// %ListOfObjects per-element export errors when mapping is invalid (accumulateErrors=true) +Method TestPersistentListOfObjectsErrorAccumulationInvalidMapping() +{ + Set list = ..BuildListOfObjectsPersistent(2) + Kill arr,errorLog + Set sc = ##class(UnitTest.isc.json.sample.generalPersistent).%JSONExportArray(list, "AltMap", {}, .arr, .errorLog) + Do $$$AssertStatusNotOK(sc) + // No entries exported as mapping is invalid for all + Do $$$AssertEquals(arr.%Size(),0) + // Should log errors for indices 0 and 1 (0-based) + Do $$$AssertTrue($Data(errorLog(0))>0) + Do $$$AssertTrue($Data(errorLog(1))>0) +} + +/// %ListOfObjects stops at first error when accumulateErrors=false and mapping invalid +Method TestPersistentListOfObjectsStopOnFirstErrorInvalidMapping() +{ + Set list = ..BuildListOfObjectsPersistent(3) + Kill arr,errorLog + Set behavior = {"accumulateErrors":0} + Set sc = ##class(UnitTest.isc.json.sample.generalPersistent).%JSONExportArray(list, "AltMap", behavior, .arr, .errorLog) + Do $$$AssertStatusNotOK(sc) + Do $$$AssertEquals(arr.%Size(),0) + Do $$$AssertTrue($Data(errorLog(0))>0) +} + +/// %SQL.StatementResult per-row export errors when mapping invalid (accumulateErrors=true) +Method TestPersistentSQLResultErrorAccumulationInvalidMapping() +{ + Set id1 = ..CreateSavedPersistent("QS1", 91) + Set id2 = ..CreateSavedPersistent("QS2", 92) + Set rs = ##class(%SQL.Statement).%ExecDirect(, "SELECT ID FROM UnitTest_isc_json_sample.generalPersistent WHERE ID IN ("_id1_","_id2_") ORDER BY ID") + $$$ThrowSQLIfError(rs.%SQLCODE, rs.%Message) + Kill arr,errorLog + Set sc = ##class(UnitTest.isc.json.sample.generalPersistent).%JSONExportArray(rs, "AltMap", {}, .arr, .errorLog) + Do $$$AssertStatusNotOK(sc) + Do $$$AssertEquals(arr.%Size(),0) + // Expect errors for both returned rows: indices 0 and 1 + Do $$$AssertTrue($Data(errorLog(0))>0) + Do $$$AssertTrue($Data(errorLog(1))>0) +} + +/// %SQL.StatementResult stops at first error when accumulateErrors=false and mapping invalid +Method TestPersistentSQLResultStopOnFirstErrorInvalidMapping() +{ + Set id1 = ..CreateSavedPersistent("QS3", 93) + Set id2 = ..CreateSavedPersistent("QS4", 94) + Set rs = ##class(%SQL.Statement).%ExecDirect(, "SELECT ID FROM UnitTest_isc_json_sample.generalPersistent WHERE ID IN ("_id1_","_id2_") ORDER BY ID") + $$$ThrowSQLIfError(rs.%SQLCODE, rs.%Message) + Kill arr,errorLog + Set behavior = {"accumulateErrors":0} + Set sc = ##class(UnitTest.isc.json.sample.generalPersistent).%JSONExportArray(rs, "AltMap", behavior, .arr, .errorLog) + Do $$$AssertStatusNotOK(sc) + Do $$$AssertEquals(arr.%Size(),0) + Set cnt=0,k="" For Set k=$Order(errorLog(k)) Quit:(k="") Set cnt=cnt+1 + Do $$$AssertEquals(cnt,1) + Do $$$AssertTrue($Data(errorLog(0))>0) +} + +/// Accepts %DynamicArray of IDs for persistent classes +Method TestPersistentDynamicArrayIDs() +{ + Set id1 = ..CreateSavedPersistent("A1", 51) + Set id2 = ..CreateSavedPersistent("A2", 52) + Set ids = [] + Do ids.%Push(id1) + Do ids.%Push(id2) + Kill arr,errorLog + Set sc = ##class(UnitTest.isc.json.sample.generalPersistent).%JSONExportArray(ids, "", {}, .arr, .errorLog) + Do $$$AssertStatusOK(sc) + Do $$$AssertEquals(arr.%Size(), 2) + Do $$$AssertEquals(arr.%Get(0).%Get("Name"), "A1") + Do $$$AssertEquals(arr.%Get(1).%Get("Name"), "A2") + Do $$$AssertEquals($Data(errorLog),0) +} + +/// Accepts %SQL.StatementResult of IDs for persistent classes +Method TestPersistentSQLStatementResultIDs() +{ + // Create specific records for query + Set id1 = ..CreateSavedPersistent("S1", 61) + Set id2 = ..CreateSavedPersistent("S2", 62) + // Select only the two we created above + Set rs = ##class(%SQL.Statement).%ExecDirect(, "SELECT ID FROM UnitTest_isc_json_sample.generalPersistent WHERE Name %STARTSWITH 'S' ORDER BY ID") + $$$ThrowSQLIfError(rs.%SQLCODE, rs.%Message) + Kill arr,errorLog + Set sc = ##class(UnitTest.isc.json.sample.generalPersistent).%JSONExportArray(rs, "", {}, .arr, .errorLog) + Do $$$AssertStatusOK(sc) + // At least the two just created should be present at the end;size may include prior S* rows if any + Do $$$AssertTrue(arr.%Size() >= 2) + // Find last two elements + Set last = (arr.%Size()-1) + Set prev = (arr.%Size()-2) + Do $$$AssertEquals(arr.%Get(prev).%Get("Name"), "S1") + Do $$$AssertEquals(arr.%Get(last).%Get("Name"), "S2") + Do $$$AssertEquals($Data(errorLog),0) +} + +/// Mapping NameOnly applied when exporting array of IDs +Method TestWithNameOnlyMapping() +{ + Set id1 = ..CreateSavedPersistent("M1", 21) + Set id2 = ..CreateSavedPersistent("M2", 22) + Set idlist = $Listbuild(id1, id2) + Kill arr,errorLog + Set sc = ##class(UnitTest.isc.json.sample.generalPersistent).%JSONExportArray(idlist, "NameOnly", {}, .arr, .errorLog) + Do $$$AssertStatusOK(sc) + Do $$$AssertEquals(arr.%Size(), 2) + Do $$$AssertEquals(arr.%Get(0).%Get("name"), "M1") + Do $$$AssertEquals(arr.%Get(1).%Get("name"), "M2") + Do $$$AssertEquals(arr.%Get(0).%IsDefined("Age"), 0) + Do $$$AssertEquals($Data(errorLog),0) +} + +/// Non-persistent classes accept only %ListOfObjects of instances +Method TestNonPersistentListOfObjectsOnly() +{ + // Build list of objects for non-persistent class + Set list = ##class(%ListOfObjects).%New() + Set list.ElementType = "UnitTest.isc.json.sample.generalNonPersistent" + For i=1:1:2 { + Set o = ##class(UnitTest.isc.json.sample.generalNonPersistent).%New() + Set o.Name = ("NP"_i) + Do o.Tags.Insert("t") + Do list.Insert(o) + } + Kill arr,errorLog + Set sc = ##class(UnitTest.isc.json.sample.generalNonPersistent).%JSONExportArray(list, "", {}, .arr, .errorLog) + Do $$$AssertStatusOK(sc) + Do $$$AssertEquals(arr.%Size(), 2) + Do $$$AssertEquals(arr.%Get(0).%Get("Name"), "NP1") + Do $$$AssertEquals($Data(errorLog),0) +} + +/// Non-persistent classes reject ID-based inputs +Method TestNonPersistentRejectsIDs() +{ + Set idlist = $Listbuild(1,2,3) + Kill arr,errorLog + Set sc = ##class(UnitTest.isc.json.sample.generalNonPersistent).%JSONExportArray(idlist, "", {}, .arr, .errorLog) + Do $$$AssertStatusNotOK(sc) + Do $$$AssertEquals($System.Status.GetErrorCodes(sc),$$$JSONInvalidListForExportArray) +} + +/// Non-persistent rejects %ListOfDataTypes +Method TestNonPersistentRejectsListOfDataTypes() +{ + Set lod = ##class(%ListOfDataTypes).%New() + Set lod.ElementType = "%Integer" Do lod.Insert(1) + Kill arr,errorLog + Set sc = ##class(UnitTest.isc.json.sample.generalNonPersistent).%JSONExportArray(lod, "", {}, .arr, .errorLog) + Do $$$AssertStatusNotOK(sc) + Do $$$AssertEquals($System.Status.GetErrorCodes(sc),$$$JSONInvalidListForExportArray) +} + +/// Non-persistent rejects %DynamicArray of IDs +Method TestNonPersistentRejectsDynamicArrayIDs() +{ + Set ids = [] Do ids.%Push(1) + Kill arr,errorLog + Set sc = ##class(UnitTest.isc.json.sample.generalNonPersistent).%JSONExportArray(ids, "", {}, .arr, .errorLog) + Do $$$AssertStatusNotOK(sc) + Do $$$AssertEquals($System.Status.GetErrorCodes(sc),$$$JSONInvalidListForExportArray) +} + +/// Non-persistent rejects %SQL.StatementResult +Method TestNonPersistentRejectsSQLStatementResult() +{ + Set rs = ##class(%SQL.Statement).%ExecDirect(, "SELECT ID FROM UnitTest_isc_json_sample.generalPersistent WHERE 1=0") + $$$ThrowSQLIfError(rs.%SQLCODE, rs.%Message) + Kill arr,errorLog + Set sc = ##class(UnitTest.isc.json.sample.generalNonPersistent).%JSONExportArray(rs, "", {}, .arr, .errorLog) + Do $$$AssertStatusNotOK(sc) + Do $$$AssertEquals($System.Status.GetErrorCodes(sc),$$$JSONInvalidListForExportArray) +} + +/// Invalid input object type is rejected (neither supported list nor IDs) +Method TestInvalidInputTypeRejectedPersistent() +{ + Set bad = ##class(UnitTest.isc.json.notJSONAdapted).%New() + Kill arr,errorLog + Set sc = ##class(UnitTest.isc.json.sample.generalPersistent).%JSONExportArray(bad, "", {}, .arr, .errorLog) + Do $$$AssertStatusNotOK(sc) + Do $$$AssertEquals($System.Status.GetErrorCodes(sc),$$$JSONInvalidListForExportArray) +} + +/// Invalid mapping name is rejected +Method TestMappingNameAccepted() +{ + Set list = ..BuildListOfObjectsPersistent(1) + Kill arr,errorLog + Set sc = ##class(UnitTest.isc.json.sample.generalPersistent).%JSONExportArray(list, "AltMap", {}, .arr, .errorLog) + Do $$$AssertStatusNotOK(sc,"Invalid mapping name should be rejected") + Do $$$AssertEquals($System.Status.GetErrorCodes(sc),$$$JSONInvalidMapping,"Correct error status for invalid mapping") +} + +/// accumulateErrors=true (default) collects per-element export errors and returns aggregated %Status +Method TestErrorAccumulation() +{ + // Create one valid, one invalid ID using %DynamicArray to ensure index tracking + Set validId = ..CreateSavedPersistent("EA_OK", 33) + Set invalidId = 99999999 // assume no such row + Set ids = [] Do ids.%Push(validId) Do ids.%Push(invalidId) + Kill arr,errorLog + Set sc = ##class(UnitTest.isc.json.sample.generalPersistent).%JSONExportArray(ids, "", {}, .arr, .errorLog) + Do $$$AssertStatusNotOK(sc) + // Array should include first exported object only + Do $$$AssertEquals(arr.%Size(),1) + Do $$$AssertEquals(arr.%Get(0).%Get("Name"),"EA_OK") + // Error logged for index 1 (second element) + Do $$$AssertTrue($Data(errorLog(1))>0) + Do $$$AssertEquals($System.Status.GetErrorCodes(sc), $System.Status.GetErrorCodes(errorLog(1))) +} + +/// accumulateErrors=false stops at first error; errorLog contains only that index +Method TestStopOnFirstError() +{ + Set id1 = ..CreateSavedPersistent("EA1", 41) + Set badId = 99999998 + Set id3 = ..CreateSavedPersistent("EA3", 43) + Set ids = [] Do ids.%Push(id1) Do ids.%Push(badId) Do ids.%Push(id3) + Kill arr,errorLog + Set behavior = {"accumulateErrors":0} + Set sc = ##class(UnitTest.isc.json.sample.generalPersistent).%JSONExportArray(ids, "", behavior, .arr, .errorLog) + Do $$$AssertStatusNotOK(sc) + // Should stop after badId; only first record exported + Do $$$AssertEquals(arr.%Size(),1) + Do $$$AssertEquals(arr.%Get(0).%Get("Name"),"EA1") + Do $$$AssertTrue($Data(errorLog(1))>0) + Do $$$AssertEquals($Order(errorLog(1)),"") + Do $$$AssertEquals($System.Status.GetErrorCodes(sc), $System.Status.GetErrorCodes(errorLog(1))) +} + +} diff --git a/internal/testing/unit_tests/UnitTest/isc/json/import.cls b/internal/testing/unit_tests/UnitTest/isc/json/import.cls new file mode 100644 index 0000000..ddd7f6c --- /dev/null +++ b/internal/testing/unit_tests/UnitTest/isc/json/import.cls @@ -0,0 +1,59 @@ +Include %pkg.isc.json.map + +/// Tests for %JSONImport +/// zpm "isc.json test -only -DUnitTest.Case=UnitTest.isc.json.import" +Class UnitTest.isc.json.import Extends UnitTest.isc.json.testBase +{ + +/// Same-name mapping: parent and SubItem both use "Simple" +Method TestImportWithSameNameMappingOnChild() +{ + #dim inst As UnitTest.isc.json.sample.generalPersistent + Set inst = ##class(UnitTest.isc.json.sample.generalPersistent).%New() + Set dobj = {"name":"P1","SubItem":{"code":"C1","value":11}} + Set sc = inst.%JSONImport(dobj,"Simple") + Do $$$AssertStatusOK(sc) + Do $$$AssertEquals(inst.Name,"P1") + Do $$$AssertTrue($IsObject(inst.SubItem)) + Do $$$AssertEquals(inst.SubItem.Code,"C1") + Do $$$AssertEquals(inst.SubItem.Value,11) +} + +/// Different-name mapping: parent chooses child's "CodeOnly" +Method TestImportWithDifferentChildMapping() +{ + #dim inst As UnitTest.isc.json.sample.generalPersistent + Set inst = ##class(UnitTest.isc.json.sample.generalPersistent).%New() + // Provide both code and value; child mapping CodeOnly should only apply Code + Set dobj = {"Name":"P2","SubItem":{"code":"C2","value":999}} + Set sc = inst.%JSONImport(dobj,"ParentChildAlt") + Do $$$AssertStatusOK(sc) + Do $$$AssertEquals(inst.Name,"P2") + Do $$$AssertEquals(inst.SubItem.Code,"C2") + // Value should be left unset by CodeOnly mapping + Do $$$AssertEquals(inst.SubItem.Value,"") +} + +/// Invalid child mapping referenced for SubItem should be rejected +Method TestImportInvalidChildMappingForSubItem() +{ + #dim inst As UnitTest.isc.json.sample.generalPersistent + Set inst = ##class(UnitTest.isc.json.sample.generalPersistent).%New() + Set dobj = {"Name":"Bad","SubItem":{"code":"C3","value":3}} + Set sc = inst.%JSONImport(dobj,"InvalidChildMap") + Do $$$AssertStatusNotOK(sc) + Do $$$AssertEquals($System.Status.GetErrorCodes(sc),$$$JSONInvalidMapping) +} + +/// Invalid child mapping referenced for SubItemPersistent should be rejected +Method TestImportInvalidChildMappingForSubItemPersistent() +{ + #dim inst As UnitTest.isc.json.sample.generalPersistent + Set inst = ##class(UnitTest.isc.json.sample.generalPersistent).%New() + Set dobj = {"Name":"Bad2","SubItemPersistent":{"code":"PC","value":4}} + Set sc = inst.%JSONImport(dobj,"InvalidChildMap") + Do $$$AssertStatusNotOK(sc) + Do $$$AssertEquals($System.Status.GetErrorCodes(sc),$$$JSONInvalidMapping) +} + +} diff --git a/internal/testing/unit_tests/UnitTest/isc/json/importArray.cls b/internal/testing/unit_tests/UnitTest/isc/json/importArray.cls new file mode 100644 index 0000000..6b4f4a5 --- /dev/null +++ b/internal/testing/unit_tests/UnitTest/isc/json/importArray.cls @@ -0,0 +1,262 @@ +/// Tests for %JSONImportArray +/// zpm "isc.json test -only -v -DUnitTest.Case=UnitTest.isc.json.exportArray" +Class UnitTest.isc.json.importArray Extends UnitTest.isc.json.testBase +{ + +/// Build %DynamicArray for generalPersistent with simple fields only +Method BuildGeneralJSONArray(count As %Integer = 3) As %DynamicArray +{ + Set arr = [] + For i=1:1:count { + Set o = { + "Name": ("User"_i), + "Age": (20+i), + "Tags": [("t"_i), "x"], + "SubList": [ + { + "Code": ("Code"_i), + "Value": (i) + }, + { + "Code": ("Code"_(i*10)), + "Value": (i*10) + } + ], + "Attr": { + "key1": ("val"_i), + "key2": "extra" + }, + "Items": { + "first": { "Code": ("C"_i), "Value": (i*10) }, + "second": { "Code": ("D"_i), "Value": (i*10+5) } + }, + "Children": [ + { "Name": ("ChildA"_i) }, + { "Name": ("ChildB"_i) } + ], + "Relateds": [ + { "Label": ("RelA"_i) }, + { "Label": ("RelB"_i) } + ] + } + Do o.%Set("Active", (i#2), "boolean") + // Add attr, child and related programmatically to exercise structures + #dim attr As %DynamicObject + #dim children As %DynamicArray + #dim relateds As %DynamicArray + Set attr = o.%Get("Attr") + If $IsObject(attr) { + Do attr.%Set("key3", ("valExtra"_i)) + } + Set children = o.%Get("Children") + If $IsObject(children) { + Do children.%Push({ "Name": ("ChildC"_i) }) + } + Set relateds = o.%Get("Relateds") + If $IsObject(relateds) { + Do relateds.%Push({ "Label": ("RelC"_i) }) + } + Do arr.%Push(o) + } + Quit arr +} + +/// Build %DynamicArray for NonPersistentSample +Method BuildNonPersistentJSONArray() As %DynamicArray +{ + Return [ + { + "Name": "Alpha", + "Tags": ["a","b"] + }, + { + "Name": "Beta", + "Tags": ["b"] + } + ] +} + +/// Creates list when not provided and populates it +Method TestNoProvidedListCreatesAndPopulates() +{ + Set arr = ..BuildGeneralJSONArray(2) + Kill list + // mappingName:"", default behavior (accumulateErrors:true, save:false) + Set sc = ##class(UnitTest.isc.json.sample.generalPersistent).%JSONImportArray(arr, "", {}, .list, .errorLog) + Do $$$AssertStatusOK(sc, "Import should succeed") + Do $$$AssertTrue($IsObject(list),"List-of-objects output must be created") + Do $$$AssertEquals(list.Count(),2,"List should contain all imported records") + + Set o1 = list.GetAt(1) + Do $$$AssertEquals(o1.Name,"User1") + Do $$$AssertEquals(o1.Age,21) + Do $$$AssertEquals(o1.Active,1) + Do $$$AssertEquals(o1.Tags.Count(),2) + + // save=0 -> not persisted + Do $$$AssertTrue(o1.%Id()="", "Objects shouldn't be saved when save=0") +} + +/// Appends to provided list +Method TestAppendsToProvidedList() +{ + Set seed = ##class(UnitTest.isc.json.sample.generalPersistent).%New() + Set seed.Name = "Seed" + Set list = ##class(%ListOfObjects).%New() + Do list.Insert(seed) + + Set arr = ..BuildGeneralJSONArray(3) + Set sc = ##class(UnitTest.isc.json.sample.generalPersistent).%JSONImportArray(arr, "", {}, .list, .errorLog) + Do $$$AssertStatusOK(sc) + Do $$$AssertEquals(list.Count(),4,"Should append 3 to existing 1") + Do $$$AssertEquals(list.GetAt(2).Name,"User1") + Do $$$AssertEquals(list.GetAt(4).Name,"User3") +} + +/// save=1 persists for persistent classes +Method TestSaveTruePersists() +{ + Set before = ##class(UnitTest.isc.json.sample.generalPersistent).Count() + Set arr = ..BuildGeneralJSONArray(2) + Kill list + // behavior.save=true + Set behavior = {"save":1} + Set sc = ##class(UnitTest.isc.json.sample.generalPersistent).%JSONImportArray(arr, "", behavior, .list, .errorLog) + Do $$$AssertStatusOK(sc) + Do $$$AssertEquals(list.Count(),2) + + Set after = ##class(UnitTest.isc.json.sample.generalPersistent).Count() + Do $$$AssertEquals(after, before+2, "Extent should increase by number imported with save=1") + + Set o = list.GetAt(1) + Do $$$AssertTrue(+o.%Id()>0,"Saved object should have an ID") +} + +/// save flag ignored for non-persistent classes (no error) +Method TestSaveIgnoredForNonPersistent() +{ + Set arr = ..BuildNonPersistentJSONArray() + Kill list + // behavior.save=true but ignored for non-persistent + Set sc = ##class(UnitTest.isc.json.sample.generalNonPersistent).%JSONImportArray(arr, "", {"save":1}, .list, .errorLog) + Do $$$AssertStatusOK(sc) + Do $$$AssertEquals(list.Count(),2) + Set np = list.GetAt(1) + Do $$$AssertEquals(np.Name,"Alpha") + Do $$$AssertEquals(np.Tags.Count(),2) +} + +/// Empty array yields success and creates empty list +Method TestEmptyArray() +{ + Set empty = [] + Kill list + Set sc = ##class(UnitTest.isc.json.sample.generalPersistent).%JSONImportArray(empty, "", {}, .list, .errorLog) + Do $$$AssertStatusOK(sc) + Do $$$AssertTrue($IsObject(list),"List should be created even for empty input") + Do $$$AssertEquals(list.Count(),0,"No records should be added for empty input") +} + +/// Invalid mapping name is rejected +Method TestInvalidMappingNameRejected() +{ + Set arr = ..BuildGeneralJSONArray(1) + Kill list + Set sc = ##class(UnitTest.isc.json.sample.generalPersistent).%JSONImportArray(arr, "AltMap", {}, .list, .errorLog) + Do $$$AssertStatusNotOK(sc,"Invalid mapping name should be rejected") + Do $$$AssertEquals($System.Status.GetErrorCodes(sc),$$$JSONInvalidMapping,"Correct error status for invalid mapping") +} + +/// NameAge mapping imports only Name and Age fields +Method TestImportWithNameAgeMapping() +{ + // Build array with extra fields that should be ignored by mapping + Set arr = [] + Do arr.%Push({"name":"U1","age":31,"Active":1,"IgnoreMe":"x"}) + Do arr.%Push({"name":"U2","age":32,"jsonObj":{},"Tags":["t"]}) + Kill list + Set sc = ##class(UnitTest.isc.json.sample.generalPersistent).%JSONImportArray(arr, "NameAge", {}, .list, .errorLog) + Do $$$AssertStatusOK(sc) + Do $$$AssertEquals(list.Count(),2) + Set o1 = list.GetAt(1) + Do $$$AssertEquals(o1.Name,"U1") + Do $$$AssertEquals(o1.Age,31) + Do $$$AssertEquals(o1.Active,"") + Do $$$AssertEquals(o1.Tags.Count(),0) +} + +/// NoStreams mapping ignores stream inputs +Method TestImportWithNoStreamsMapping() +{ + Set arr = [] + Do arr.%Push({"Name":"SLESS","Age":20,"CharStream":"X","BinaryStream":"Y","BinaryStreamHex":"Z"}) + Kill list + Set sc = ##class(UnitTest.isc.json.sample.generalPersistent).%JSONImportArray(arr, "NoStreams", {}, .list, .errorLog) + Do $$$AssertStatusOK(sc) + #dim o as UnitTest.isc.json.sample.generalPersistent + Set o = list.GetAt(1) + Do $$$AssertEquals(o.Name,"SLESS") + Do $$$AssertEquals(o.CharStream.Size,0) + Do $$$AssertEquals(o.BinaryStream.Size,0) + Do $$$AssertEquals(o.BinaryStreamHex.Size,0) +} + +/// accumulateErrors=true (default) collects per-element errors and returns aggregated %Status +Method TestImportArrayErrorAccumulation() +{ + // First element valid, second element invalid (missing required Name) + Set arr = [] + Do arr.%Push({"Name":"OK","Age":30}) + Do arr.%Push({"Age":31}) + Kill list,errorLog + Set sc = ##class(UnitTest.isc.json.sample.generalPersistent).%JSONImportArray(arr, "", {}, .list, .errorLog) + Do $$$AssertStatusNotOK(sc) + // List is still created and both elements inserted + Do $$$AssertTrue($IsObject(list)) + Do $$$AssertEquals(list.Count(),2) + // Error is logged for index 1 (second element, 0-based) + Do $$$AssertTrue($Data(errorLog(1))>0) + Do $$$AssertEquals($System.Status.GetErrorCodes(sc), $System.Status.GetErrorCodes(errorLog(1))) +} + +/// accumulateErrors=false stops at first error; errorLog contains only that index +Method TestImportArrayStopOnFirstError() +{ + Set arr = [] + Do arr.%Push({"Name":"OK1","Age":25}) + Do arr.%Push({"Age":26}) // invalid: missing Name + Do arr.%Push({"Name":"OK2","Age":27}) + Kill list,errorLog + Set behavior = {"accumulateErrors":0} + Set sc = ##class(UnitTest.isc.json.sample.generalPersistent).%JSONImportArray(arr, "", behavior, .list, .errorLog) + Do $$$AssertStatusNotOK(sc) + // Should stop after second element; list has at least two entries + Do $$$AssertTrue(list.Count()>=2) + // Count error entries and ensure it is index 1 (second element) + Set cnt=0,k="" For Set k=$Order(errorLog(k)) Quit:(k="") Set cnt=cnt+1 + Do $$$AssertEquals(cnt,1,"Only one error should be logged") + Do $$$AssertTrue($Data(errorLog(1))>0) + Do $$$AssertEquals($System.Status.GetErrorCodes(sc), $System.Status.GetErrorCodes(errorLog(1))) +} + +/// Mixed malformed/valid inputs produce errorLog entries at exact indices +Method TestImportArrayMixedValidityErrorIndices() +{ + // 0: valid, 1: invalid (missing Name), 2: valid, 3: invalid type, 4: valid + Set arr = [] + Do arr.%Push({"Name":"OK0","Age":20}) + Do arr.%Push({"Age":21}) // missing Name + Do arr.%Push({"Name":"OK2","Age":22}) + Do arr.%Push("notAnObject") + Do arr.%Push({"Name":"OK4","Age":24}) + Kill list,errorLog + // Default accumulateErrors=true; expect both errors captured + Set sc = ##class(UnitTest.isc.json.sample.generalPersistent).%JSONImportArray(arr, "", {}, .list, .errorLog) + Do $$$AssertStatusNotOK(sc) + Do $$$AssertEquals(list.Count(),5) + // Errors should be at indices 1 and 3 (0-based) + Do $$$AssertTrue($Data(errorLog(1))>0) + Do $$$AssertTrue($Data(errorLog(3))>0) +} + +} diff --git a/internal/testing/unit_tests/UnitTest/isc/json/jsonNew.cls b/internal/testing/unit_tests/UnitTest/isc/json/jsonNew.cls new file mode 100644 index 0000000..76ef69b --- /dev/null +++ b/internal/testing/unit_tests/UnitTest/isc/json/jsonNew.cls @@ -0,0 +1,143 @@ +/// Test %JSONNew code generation and overrides +Class UnitTest.isc.json.jsonNew Extends %UnitTest.TestCase +{ + +Property tlevel As %Integer; + +Method OnBeforeAllTests() As %Status +{ + Set ..tlevel = $TLevel + TSTART + Set sc = $$$OK + Try { + // Create mock data + Set sc = ##class(UnitTest.isc.json.sample.jsonNewPersistent).%KillExtent() + $$$ThrowOnError(sc) + For i = 1:1:10 { + Set o = ##class(UnitTest.isc.json.sample.jsonNewPersistent).%New() + Set o.Foo = "Foo:"_i + Set o.Bar = "Bar:"_i + Set o.Baz = "Baz:"_i + Set o.Another = "Another:"_i + Set o.Combined1 = "Combined1:"_i + Set o.Combined2 = "Combined2:"_i + $$$ThrowOnError(o.%Save()) + Kill o + } + } Catch (ex) { + Set sc = ex.AsStatus() + } + Return sc +} + +Method OnAfterAllTests() As %Status +{ + While ($TLevel > ..tlevel) { + TROLLBACK 1 + } + Return $$$OK +} + +Method TestNonPersistent() +{ + Set json = { "Foo": "nonPersistentFoo", "Bar": "nonPersistentBar", "Another": "nonPersistentAnother" } + Set obj = ##class(UnitTest.isc.json.sample.jsonNew).%JSONNew(json) + Do $$$AssertEquals(obj.Foo,"") + Do $$$AssertEquals(obj.Bar,"") + Do $$$AssertEquals(obj.Another,"") + + // JSONNew override + Set json = { "_includeDefaults": true } + Set obj = ##class(UnitTest.isc.json.sample.jsonNew).%JSONNew(json) + Do $$$AssertEquals(obj.Foo,"DefaultFoo") + Do $$$AssertEquals(obj.Bar,"DefaultBar") +} + +Method TestPersistent() +{ + // Insufficient matching info + Set json = { "Another": "Another:1" } + Set obj = ##class(UnitTest.isc.json.sample.jsonNewPersistent).%JSONNew(json) + Do $$$AssertEquals(obj.%Id(),"") + Set json = { "Foo": "Foo:1", "Combined1": "Combined1:1" } + Set obj = ##class(UnitTest.isc.json.sample.jsonNewPersistent).%JSONNew(json) + Do $$$AssertEquals(obj.%Id(),"") + Set json = { + "Foo": "Foo:19", "Bar": "Bar:19", + "Combined1": "Combined1:28", "Combined2": "Combined2:28", + "Baz": "Baz:37" + } + Set obj = ##class(UnitTest.isc.json.sample.jsonNewPersistent).%JSONNew(json) + Do $$$AssertEquals(obj.%Id(),"") + Set json = { + "Foo": "Foo:1", "Bar": "Bar:1", + "Combined1": "Combined1:2", "Combined2": "Combined2:2", + "Baz": "Baz:3" + } + Set obj = ##class(UnitTest.isc.json.sample.jsonNewPersistent).%JSONNew(json,,"UniqueIndexPropsAbsent") + Do $$$AssertEquals(obj.%Id(),"") + + // Record matching ID + Set id = "Foo:1||Bar:1" + Set json = { "myID": (id) } + Set obj = ##class(UnitTest.isc.json.sample.jsonNewPersistent).%JSONNew(json,,"IdIncluded") + Do $$$AssertEquals(obj.%Id(),id) + Set json = { "myID": 42 } + Set obj = ##class(UnitTest.isc.json.sample.jsonNewPersistent).%JSONNew(json,,"IdIncluded") + Do $$$AssertEquals(obj.%Id(),"") + + // Record matching IdKey + Set json = { "Foo": "Foo:3", "Bar": "Bar:3" } + Set obj = ##class(UnitTest.isc.json.sample.jsonNewPersistent).%JSONNew(json) + Do $$$AssertEquals(obj.%Id(),"Foo:3||Bar:3") + Set json = { "Foo": "Foo:42", "Bar": "Bar:42" } + Set obj = ##class(UnitTest.isc.json.sample.jsonNewPersistent).%JSONNew(json) + Do $$$AssertEquals(obj.%Id(),"") + + // Record matching unique index Baz + Set json = { "Baz": "Baz:5" } + Set obj = ##class(UnitTest.isc.json.sample.jsonNewPersistent).%JSONNew(json) + Do $$$AssertEquals(obj.%Id(),"Foo:5||Bar:5") + Set json = { "Baz": "Baz:777" } + Set obj = ##class(UnitTest.isc.json.sample.jsonNewPersistent).%JSONNew(json) + Do $$$AssertEquals(obj.%Id(),"") + + // Record matching unique index Combined + Set json = { "Combined1": "Combined1:7", "Combined2": "Combined2:7" } + Set obj = ##class(UnitTest.isc.json.sample.jsonNewPersistent).%JSONNew(json) + Do $$$AssertEquals(obj.%Id(),"Foo:7||Bar:7") + Set json = { "Combined1": "Combined1:71", "Combined2": "Combined2:71" } + Set obj = ##class(UnitTest.isc.json.sample.jsonNewPersistent).%JSONNew(json) + Do $$$AssertEquals(obj.%Id(),"") + + // Record matching combinations of indices + Set json = { + "Combined1": "Combined1:2", "Combined2": "Combined2:2", + "Baz": "Baz:3" + } + Set obj = ##class(UnitTest.isc.json.sample.jsonNewPersistent).%JSONNew(json) + Do $$$AssertEquals(obj.%Id(),"Foo:2||Bar:2") + Set json = { + "Foo": "Foo:1", "Bar": "Bar:1", + "Combined1": "Combined1:2", "Combined2": "Combined2:2", + "Baz": "Baz:3" + } + Set obj = ##class(UnitTest.isc.json.sample.jsonNewPersistent).%JSONNew(json) + Do $$$AssertEquals(obj.%Id(),"Foo:1||Bar:1") + Set json = { + "myID": "Foo:4||Bar:4", + "Foo": "Foo:1", "Bar": "Bar:1", + "Combined1": "Combined1:2", "Combined2": "Combined2:2", + "Baz": "Baz:3" + } + Set obj = ##class(UnitTest.isc.json.sample.jsonNewPersistent).%JSONNew(json,,"IdIncluded") + Do $$$AssertEquals(obj.%Id(),"Foo:4||Bar:4") + Do json.%Set("_cloneExisting", 1, "boolean") + Set obj = ##class(UnitTest.isc.json.sample.jsonNewPersistent).%JSONNew(json,,"IdIncluded") + Do $$$AssertEquals(obj.%Id(),"") + + // Overridden +} + +} + diff --git a/internal/testing/unit_tests/UnitTest/isc/json/mappingOverlayTest.cls b/internal/testing/unit_tests/UnitTest/isc/json/mappingOverlayTest.cls index 46d5977..6bd39b1 100644 --- a/internal/testing/unit_tests/UnitTest/isc/json/mappingOverlayTest.cls +++ b/internal/testing/unit_tests/UnitTest/isc/json/mappingOverlayTest.cls @@ -1,3 +1,4 @@ +/// zpm "isc.json test -only -DUnitTest.Case=UnitTest.isc.json.mappingOverlayTest" Class UnitTest.isc.json.mappingOverlayTest Extends %UnitTest.TestCase { @@ -74,5 +75,100 @@ Method GetImportedObject(mapping As %String, object As %DynamicAbstractObject = Quit inst } +Method TestMappingInfoDefault() +{ + #dim mappingInfo As %pkg.isc.json.mappingInfo + Set sc = ##class(UnitTest.isc.json.mappingOverlay).%JSONMappingInfo(.mappingInfo) + Do $$$AssertStatusOK(sc) + Do $$$AssertEquals(mappingInfo.Classname,"UnitTest.isc.json.mappingOverlay") + Do $$$AssertEquals(mappingInfo.IgnoreInvalidField,0) + Do $$$AssertEquals(mappingInfo.Properties.Count(),3) + Set name = mappingInfo.Properties.GetAt(1) + Do $$$AssertEquals(name.Name,"Name") + Do $$$AssertEquals(name.FieldName,"Name") + Set dob = mappingInfo.Properties.GetAt(2) + Do $$$AssertEquals(dob.Name,"DOB") + Do $$$AssertEquals(dob.FieldName,"DOB") + Set ssn = mappingInfo.Properties.GetAt(3) + Do $$$AssertEquals(ssn.Name,"SSN") + Do $$$AssertEquals(ssn.FieldName,"SSN") +} + +Method TestMappingInfoNameOnly() +{ + #dim mappingInfo As %pkg.isc.json.mappingInfo + Set sc = ##class(UnitTest.isc.json.mappingOverlay).%JSONMappingInfo(.mappingInfo,"NameOnly") + Do $$$AssertStatusOK(sc) + Do $$$AssertEquals(mappingInfo.Classname,"UnitTest.isc.json.mappingOverlay") + Do $$$AssertEquals(mappingInfo.IgnoreInvalidField,1) + Do $$$AssertEquals(mappingInfo.Properties.Count(),1) + Set name = mappingInfo.Properties.GetAt(1) + Do $$$AssertEquals(name.Name,"Name") + Do $$$AssertEquals(name.FieldName,"name") +} + +Method TestMappingInfoEverything() +{ + #dim mappingInfo As %pkg.isc.json.mappingInfo + Set sc = ##class(UnitTest.isc.json.mappingOverlay).%JSONMappingInfo(.mappingInfo,"Everything") + Do $$$AssertStatusOK(sc) + Do $$$AssertEquals(mappingInfo.Classname,"UnitTest.isc.json.mappingOverlay") + Do $$$AssertEquals(mappingInfo.IgnoreInvalidField,1) + Do $$$AssertEquals(mappingInfo.Properties.Count(),3) + Set name = mappingInfo.Properties.GetAt(1) + Do $$$AssertEquals(name.Name,"Name") + Do $$$AssertEquals(name.FieldName,"name") + Set dob = mappingInfo.Properties.GetAt(2) + Do $$$AssertEquals(dob.Name,"DOB") + Do $$$AssertEquals(dob.FieldName,"dob") + Set ssn = mappingInfo.Properties.GetAt(3) + Do $$$AssertEquals(ssn.Name,"SSN") + Do $$$AssertEquals(ssn.FieldName,"ssn") +} + +Method TestMappingInfoSSNInputOnly() +{ + #dim mappingInfo As %pkg.isc.json.mappingInfo + Set sc = ##class(UnitTest.isc.json.mappingOverlay).%JSONMappingInfo(.mappingInfo,"SSNInputOnly") + Do $$$AssertStatusOK(sc) + Do $$$AssertEquals(mappingInfo.Classname,"UnitTest.isc.json.mappingOverlay") + Do $$$AssertEquals(mappingInfo.IgnoreInvalidField,1) + Do $$$AssertEquals(mappingInfo.Properties.Count(),3) + Set name = mappingInfo.Properties.GetAt(1) + Do $$$AssertEquals(name.Name,"Name") + Do $$$AssertEquals(name.FieldName,"Name") + Set dob = mappingInfo.Properties.GetAt(2) + Do $$$AssertEquals(dob.Name,"DOB") + Do $$$AssertEquals(dob.FieldName,"DOB") + Set ssn = mappingInfo.Properties.GetAt(3) + Do $$$AssertEquals(ssn.Name,"SSN") + Do $$$AssertEquals(ssn.FieldName,"SSN") + Do $$$AssertEquals(ssn.Include,"inputonly") +} + +Method TestMappingInfoNoSSN() +{ + #dim mappingInfo As %pkg.isc.json.mappingInfo + Set sc = ##class(UnitTest.isc.json.mappingOverlay).%JSONMappingInfo(.mappingInfo,"NoSSN") + Do $$$AssertStatusOK(sc) + Do $$$AssertEquals(mappingInfo.Classname,"UnitTest.isc.json.mappingOverlay") + Do $$$AssertEquals(mappingInfo.IgnoreInvalidField,1) + Do $$$AssertEquals(mappingInfo.Properties.Count(),2) + Set name = mappingInfo.Properties.GetAt(1) + Do $$$AssertEquals(name.Name,"Name") + Do $$$AssertEquals(name.FieldName,"Name") + Set dob = mappingInfo.Properties.GetAt(2) + Do $$$AssertEquals(dob.Name,"DOB") + Do $$$AssertEquals(dob.FieldName,"DOB") +} + +Method TestMappingInfoInvalidMapping() +{ + #dim mappingInfo As %pkg.isc.json.mappingInfo + Set sc = ##class(UnitTest.isc.json.mappingOverlay).%JSONMappingInfo(.mappingInfo,"MyInvalidMapping") + Do $$$AssertStatusOK(sc) + Do $$$AssertEquals(mappingInfo,"") +} + } diff --git a/internal/testing/unit_tests/UnitTest/isc/json/overrideRequired.cls b/internal/testing/unit_tests/UnitTest/isc/json/overrideRequired.cls index 03c7bee..1a8d04e 100644 --- a/internal/testing/unit_tests/UnitTest/isc/json/overrideRequired.cls +++ b/internal/testing/unit_tests/UnitTest/isc/json/overrideRequired.cls @@ -17,7 +17,7 @@ XData InvertedMapping [ XMLNamespace = "http://www.intersystems.com/_pkg/isc/jso Method TestCompilingMe() { // Gets Generator test coverage credit! - Do $$$AssertStatusOK($System.OBJ.Compile($classname(),"ck/nomulticompile")) + Do $$$AssertStatusOK($System.OBJ.Compile($classname(),"ck/display=none/nomulticompile")) } /// Make sure custom mapping works bidirectionally @@ -40,4 +40,4 @@ Method TestCustomMapping() do $$$AssertStatusOK(sc, "Object with only non-required properties imports using inverted mapping.") } -} \ No newline at end of file +} diff --git a/internal/testing/unit_tests/UnitTest/isc/json/sample/child.cls b/internal/testing/unit_tests/UnitTest/isc/json/sample/child.cls new file mode 100644 index 0000000..b689942 --- /dev/null +++ b/internal/testing/unit_tests/UnitTest/isc/json/sample/child.cls @@ -0,0 +1,27 @@ +Class UnitTest.isc.json.sample.child Extends (%Persistent, %pkg.isc.json.adaptor) +{ + +Property Name As %String(MAXLEN = 128); + +Relationship Parent As UnitTest.isc.json.sample.generalPersistent [ Cardinality = parent, Inverse = Children ]; + +Storage Default +{ + + +%%CLASSNAME + + +Name + + +{%%PARENT}("Children") +childDefaultData +^UnitTest.i7E30.generalPersiE85C("Children") +^UnitTest.isc.json.sa7E30.childI +^UnitTest.isc.json.sa7E30.childS +%Storage.Persistent +} + +} + diff --git a/internal/testing/unit_tests/UnitTest/isc/json/sample/generalNonPersistent.cls b/internal/testing/unit_tests/UnitTest/isc/json/sample/generalNonPersistent.cls new file mode 100644 index 0000000..f396a41 --- /dev/null +++ b/internal/testing/unit_tests/UnitTest/isc/json/sample/generalNonPersistent.cls @@ -0,0 +1,9 @@ +Class UnitTest.isc.json.sample.generalNonPersistent Extends (%RegisteredObject, %pkg.isc.json.adaptor) +{ + +Property Name As %String(MAXLEN = 80) [ Required ]; + +Property Tags As list Of %String(MAXLEN = 60); + +} + diff --git a/internal/testing/unit_tests/UnitTest/isc/json/sample/generalPersistent.cls b/internal/testing/unit_tests/UnitTest/isc/json/sample/generalPersistent.cls new file mode 100644 index 0000000..29a6dc3 --- /dev/null +++ b/internal/testing/unit_tests/UnitTest/isc/json/sample/generalPersistent.cls @@ -0,0 +1,240 @@ +Class UnitTest.isc.json.sample.generalPersistent Extends (%Persistent, %pkg.isc.json.adaptor) +{ + +// Scalars + +Property Name As %String(MAXLEN = 80) [ Required ]; + +Property NameIgnoreNull As %String(%JSONIGNORENULL = 1, MAXLEN = 80); + +Property "Snake_Case" As %String(MAXLEN = 100); + +Property Age As %Integer; + +Property DoubleVal As %Double; + +Property Active As %Boolean; + +Property Timestamp As %TimeStamp; + +Property IntOrString As UnitTest.isc.json.sample.intOrString; + +Property jsonObj As %DynamicObject; + +Property jsonArr As %DynamicArray; + +// Streams + +Property CharStream As %Stream.GlobalCharacter; + +Property BinaryStream As %Stream.GlobalBinary; + +Property BinaryStreamHex As %Stream.GlobalBinary(ENCODING = "hex"); + +// Nested objects + +Property SubItem As UnitTest.isc.json.sample.subItem; + +Property SubItemPersistent As UnitTest.isc.json.sample.subItemPersistent; + +Property SubItemPersistentID As UnitTest.isc.json.sample.subItemPersistent(%JSONREFERENCE = "ID"); + +Property SubItemPersistentOID As UnitTest.isc.json.sample.subItemPersistent(%JSONREFERENCE = "OID"); + +Property SubItemPersistentGUID As UnitTest.isc.json.sample.subItemPersistent(%JSONREFERENCE = "GUID"); + +// Lists / Arrays + +Property Tags As list Of %String(MAXLEN = 60); + +Property SubList As list Of UnitTest.isc.json.sample.subItem; + +Property Attr As array Of %String(MAXLEN = 200); + +Property Items As array Of UnitTest.isc.json.sample.subItem; + +// Persistent Lists / Arrays + +Property SubListPersistent As list Of UnitTest.isc.json.sample.subItemPersistent; + +Property ItemsPersistent As array Of UnitTest.isc.json.sample.subItemPersistent; + +// Relationships + +Relationship Children As UnitTest.isc.json.sample.child [ Cardinality = children, Inverse = Parent ]; + +Relationship Relateds As UnitTest.isc.json.sample.related [ Cardinality = many, Inverse = Owner ]; + +ClassMethod Count() +{ + #dim resultSet As %SQL.StatementResult + Set query = "SELECT Count(ID) As Total FROM UnitTest_isc_json_sample.generalPersistent" + Set resultSet = ##class(%SQL.Statement).%ExecDirect(, query) + $$$ThrowSQLIfError(resultSet.%SQLCODE,resultSet.%Message) + Do resultSet.%Next(.sc) + $$$ThrowOnError(sc) + Return resultSet.%Get("Total") +} + +Storage Default +{ + +Attr +subnode +"Attr" + + +Items +subnode +"Items" + + +ItemsPersistent +subnode +"ItemsPersistent" + + + +%%CLASSNAME + + +Name + + +Age + + +Active + + +Tags + + +SubList + + +Timestamp + + +IntOrString + + +CharStream + + +BinaryStream + + +BinaryStreamHex + + +SubItem + + +SubItemPersistent + + +SubItemPersistentID + + +SubItemPersistentOID + + +SubItemPersistentGUID + + +Snake_Case + + +NameIgnorNull + + +NameIgnoreNull + + +SubListPersistent + + +DoubleVal + + + +jsonArr +node +"jsonArr" + + +jsonObj +node +"jsonObj" + +^UnitTest.i7E30.generalPersiE85D +generalPersistentDefaultData +^UnitTest.i7E30.generalPersiE85D +^UnitTest.i7E30.generalPersiE85I +^UnitTest.i7E30.generalPersiE85S +%Storage.Persistent +} + +XData NameOnly [ XMLNamespace = "http://www.intersystems.com/_pkg/isc/json/jsonmapping" ] +{ + + + + + + + + +} + +XData NameAge [ XMLNamespace = "http://www.intersystems.com/_pkg/isc/json/jsonmapping" ] +{ + + + + + +} + +XData NoStreams [ XMLNamespace = "http://www.intersystems.com/_pkg/isc/json/jsonmapping" ] +{ + + + + + + + +} + +XData Simple [ XMLNamespace = "http://www.intersystems.com/_pkg/isc/json/jsonmapping" ] +{ + + + + + + +} + +XData ParentChildAlt [ XMLNamespace = "http://www.intersystems.com/_pkg/isc/json/jsonmapping" ] +{ + + + + + +} + +XData InvalidChildMap [ XMLNamespace = "http://www.intersystems.com/_pkg/isc/json/jsonmapping" ] +{ + + + + + + +} + +} diff --git a/internal/testing/unit_tests/UnitTest/isc/json/sample/intOrString.cls b/internal/testing/unit_tests/UnitTest/isc/json/sample/intOrString.cls new file mode 100644 index 0000000..e8e444b --- /dev/null +++ b/internal/testing/unit_tests/UnitTest/isc/json/sample/intOrString.cls @@ -0,0 +1,40 @@ +/// Can be an integer or a string. +Class UnitTest.isc.json.sample.intOrString Extends (%String, %Integer) +{ + +Parameter JSONTYPE = "runtime"; + +ClassMethod GetJSONTYPE(value As %String) As %String +{ + Set intSc = ##class(%Integer).IsValid(value) + If $$$ISOK(intSc) { + Return "number" + } + Return "string" +} + +ClassMethod IsValid(value As %String) As %Status +{ + Set intSc = ##class(%Integer).IsValid(value) + Set strSc = ##class(%String).IsValid(value) + If $$$ISERR(intSc) && $$$ISERR(strSc) { + Return $$$ADDSC(intSc,strSc) + } + Return $$$OK +} + +ClassMethod JSONToLogical(value As %String) As %String +{ + Return value +} + +ClassMethod LogicalToJSON(value As %String) As %String +{ + If ##class(%Integer).IsValid(value) { + Return value + } + Return value +} + +} + diff --git a/internal/testing/unit_tests/UnitTest/isc/json/sample/jsonNew.cls b/internal/testing/unit_tests/UnitTest/isc/json/sample/jsonNew.cls new file mode 100644 index 0000000..34183a0 --- /dev/null +++ b/internal/testing/unit_tests/UnitTest/isc/json/sample/jsonNew.cls @@ -0,0 +1,28 @@ +/// For testing %JSONNew code generation +Class UnitTest.isc.json.sample.jsonNew Extends (%RegisteredObject, %pkg.isc.json.adaptor) +{ + +Property Foo As %String(%JSONREQUIRED = 0, MAXLEN = 128) [ Required ]; + +Property Bar As %String(%JSONREQUIRED = 0, MAXLEN = 128) [ Required ]; + +Property Baz As %String(MAXLEN = 128); + +Property Another As %String(MAXLEN = 128); + +Property Combined1 As %String(MAXLEN = 128); + +Property Combined2 As %String(MAXLEN = 128); + +ClassMethod %JSONNew(dynamicObject As %DynamicObject, containerOref As %RegisteredObject = "") As %RegisteredObject +{ + Set obj = ..%JSONNewDefault(dynamicObject,containerOref) + If dynamicObject.%Get("_includeDefaults") { + Set obj.Foo = "DefaultFoo" + Set obj.Bar = "DefaultBar" + } + Return obj +} + +} + diff --git a/internal/testing/unit_tests/UnitTest/isc/json/sample/jsonNewPersistent.cls b/internal/testing/unit_tests/UnitTest/isc/json/sample/jsonNewPersistent.cls new file mode 100644 index 0000000..c905992 --- /dev/null +++ b/internal/testing/unit_tests/UnitTest/isc/json/sample/jsonNewPersistent.cls @@ -0,0 +1,77 @@ +/// For testing %JSONNew code generation +Class UnitTest.isc.json.sample.jsonNewPersistent Extends (%Persistent, UnitTest.isc.json.sample.jsonNew) +{ + +Index FooBarIdx On (Foo, Bar) [ IdKey ]; + +Index UniqueBazIdx On Baz [ Unique ]; + +Index UniqueCombinedIdx On (Combined1, Combined2) [ Unique ]; + +Index AnotherIdx On Another; + +XData IdIncluded [ XMLNamespace = "http://www.intersystems.com/_pkg/isc/json/jsonmapping" ] +{ + + + +} + +XData UniqueIndexPropsAbsent [ XMLNamespace = "http://www.intersystems.com/_pkg/isc/json/jsonmapping" ] +{ + + + + + + +} + +/// Get an instance of an JSON enabled class.

+/// +/// You may override this method to do custom processing (such as initializing +/// the object instance) before returning an instance of this class. +/// However, this method should not be called directly from user code.
+/// Arguments:
+/// dynamicObject is the dynamic object with thee values to be assigned to the new object.
+/// containerOref is the containing object instance when called from JSONImport. +/// mappingName is the name of the mapping to use for the export. The base mapping is represened by "" and is the default. +ClassMethod %JSONNew(dynamicObject As %DynamicObject, containerOref As %RegisteredObject = "", %mappingName As %String = "") As %RegisteredObject +{ + Set obj = ..%JSONNewDefault(dynamicObject, containerOref, %mappingName) + // Force returning a new object as a clone of any existing found + If (dynamicObject.%Get("_cloneExisting", 0)) { + Return obj.%ConstructClone() + } + Return obj +} + +Storage Default +{ + + +%%CLASSNAME + + +Baz + + +Another + + +Combined1 + + +Combined2 + + +^UnitTest.i7E30.jsonNewPers2F84D +jsonNewPersistentDefaultData +^UnitTest.i7E30.jsonNewPers2F84D +^UnitTest.i7E30.jsonNewPers2F84I +^UnitTest.i7E30.jsonNewPers2F84S +%Storage.Persistent +} + +} + diff --git a/internal/testing/unit_tests/UnitTest/isc/json/sample/related.cls b/internal/testing/unit_tests/UnitTest/isc/json/sample/related.cls new file mode 100644 index 0000000..ff0447c --- /dev/null +++ b/internal/testing/unit_tests/UnitTest/isc/json/sample/related.cls @@ -0,0 +1,30 @@ +Class UnitTest.isc.json.sample.related Extends (%Persistent, %pkg.isc.json.adaptor) +{ + +Property Label As %String(MAXLEN = 80); + +Relationship Owner As UnitTest.isc.json.sample.generalPersistent [ Cardinality = one, Inverse = Relateds ]; + +Storage Default +{ + + +%%CLASSNAME + + +Label + + +Owner + + +^UnitTest.isc.json7E30.relatedD +relatedDefaultData +^UnitTest.isc.json7E30.relatedD +^UnitTest.isc.json7E30.relatedI +^UnitTest.isc.json7E30.relatedS +%Storage.Persistent +} + +} + diff --git a/internal/testing/unit_tests/UnitTest/isc/json/sample/subItem.cls b/internal/testing/unit_tests/UnitTest/isc/json/sample/subItem.cls new file mode 100644 index 0000000..3aa4465 --- /dev/null +++ b/internal/testing/unit_tests/UnitTest/isc/json/sample/subItem.cls @@ -0,0 +1,40 @@ +Class UnitTest.isc.json.sample.subItem Extends (%SerialObject, %pkg.isc.json.adaptor) +{ + +Property Code As %String(MAXLEN = 64); + +Property Value As %Integer; + +Storage Default +{ + + +Code + + +Value + + +subItemState +^UnitTest.isc.json7E30.subItemS +%Storage.Serial +} + +XData Simple [ XMLNamespace = "http://www.intersystems.com/_pkg/isc/json/jsonmapping" ] +{ + + + + + +} + +XData CodeOnly [ XMLNamespace = "http://www.intersystems.com/_pkg/isc/json/jsonmapping" ] +{ + + + + +} + +} diff --git a/internal/testing/unit_tests/UnitTest/isc/json/sample/subItemPersistent.cls b/internal/testing/unit_tests/UnitTest/isc/json/sample/subItemPersistent.cls new file mode 100644 index 0000000..40cfc63 --- /dev/null +++ b/internal/testing/unit_tests/UnitTest/isc/json/sample/subItemPersistent.cls @@ -0,0 +1,26 @@ +Class UnitTest.isc.json.sample.subItemPersistent Extends (%Persistent, UnitTest.isc.json.sample.subItem) +{ + +Storage Default +{ + + +%%CLASSNAME + + +Code + + +Value + + +^UnitTest.i7E30.subItemPersDA1AD +subItemPersistentDefaultData +^UnitTest.i7E30.subItemPersDA1AD +^UnitTest.i7E30.subItemPersDA1AI +^UnitTest.i7E30.subItemPersDA1AS +%Storage.Persistent +} + +} + diff --git a/internal/testing/unit_tests/UnitTest/isc/json/testBase.cls b/internal/testing/unit_tests/UnitTest/isc/json/testBase.cls new file mode 100644 index 0000000..92eddd2 --- /dev/null +++ b/internal/testing/unit_tests/UnitTest/isc/json/testBase.cls @@ -0,0 +1,81 @@ +Include %pkg.isc.json.map + +/// Base class for unit tests to generate test data +Class UnitTest.isc.json.testBase Extends %UnitTest.TestCase +{ + +Method OnBeforeAllTests() As %Status +{ + Do ##class(UnitTest.isc.json.sample.child).%KillExtent() + Do ##class(UnitTest.isc.json.sample.related).%KillExtent() + Do ##class(UnitTest.isc.json.sample.generalPersistent).%KillExtent() + Quit $$$OK +} + +Method CreateSavedPersistent(name As %String = "User", age As %Integer = 30) As %Integer +{ + Set o = ##class(UnitTest.isc.json.sample.generalPersistent).%New() + Set o.Name = name + Set o.Age = age + Set o.DoubleVal = 123.456 + Set o.NameIgnoreNull = name_"_IGN" + Set o."Snake_Case" = name_"_snake" + Set o.Active = 1 + Set o.Timestamp = $ZDATETIME($ZTIMESTAMP,3) + Set o.IntOrString = 777 + Set o.jsonObj = {"k":"v"} + Set o.jsonArr = [1,2] + Set cs = ##class(%Stream.GlobalCharacter).%New() + Do cs.Write("chars-"_name) + Set o.CharStream = cs + Set bs = ##class(%Stream.GlobalBinary).%New() + Do bs.Write($C(1,2,3,4)) + Set o.BinaryStream = bs + Set bhex = ##class(%Stream.GlobalBinary).%New() + Do bhex.Write($C(171,205,239)) + Set o.BinaryStreamHex = bhex + Set sub = ##class(UnitTest.isc.json.sample.subItem).%New() + Set sub.Code = name_"_C" + Set sub.Value = age + Set o.SubItem = sub + Set subp = ##class(UnitTest.isc.json.sample.subItemPersistent).%New() + Set subp.Code = name_"_PC" + Set subp.Value = age+1 + $$$ThrowOnError(subp.%Save()) + Set o.SubItemPersistent = subp + Set o.SubItemPersistentID = subp + Set o.SubItemPersistentOID = subp + Set o.SubItemPersistentGUID = subp + Do o.Tags.Insert(name_"_t1") + Do o.Tags.Insert(name_"_t2") + Set sl = ##class(UnitTest.isc.json.sample.subItem).%New() + Set sl.Code = name_"_SL" + Set sl.Value = age+2 + Do o.SubList.Insert(sl) + Do o.Attr.SetAt(name_"_val1","key1") + Do o.Attr.SetAt(name_"_val2","key2") + Set it = ##class(UnitTest.isc.json.sample.subItem).%New() + Set it.Code = name_"_IT" + Set it.Value = age+3 + Do o.Items.SetAt(it,"first") + $$$ThrowOnError(o.%Save()) + Quit o.%Id() +} + +Method BuildUnsavedPersistent(name As %String = "UP", age As %Integer = 25) As UnitTest.isc.json.sample.generalPersistent +{ + Set o = ##class(UnitTest.isc.json.sample.generalPersistent).%New() + Set o.Name = name + Set o.Age = age + Quit o +} + +Method BuildUnsavedNonPersistent(name As %String = "NP") As UnitTest.isc.json.sample.generalNonPersistent +{ + Set o = ##class(UnitTest.isc.json.sample.generalNonPersistent).%New() + Set o.Name = name + Do o.Tags.Insert("t") + Quit o +} + +} diff --git a/module.xml b/module.xml index 831ba31..abbbc85 100644 --- a/module.xml +++ b/module.xml @@ -1,20 +1,26 @@ - + isc.json - 2.0.1 + 3.5.0 + + InterSystems Corporation + AppServices + module - - - - - + + + + test UnitTest.isc.json + + + pkg.isc.json.lifecycle @@ -33,4 +39,5 @@ - + + \ No newline at end of file