Many applications need to convert the internal in-memory representation of objects into other formats, in order to save them in files and databases, send them over networks, or exchange them with other applications that might be written in different languages.
In our case we will store contribution objects into plain text files.
A very common, simple and well-suited format for storing hierarchical data like object trees is JSON. This is the format we are going to use.
Here is an example of a contribution object encoded as JSON:
{"identifier":1, "first_name":"Giovanni", "last_name":"Spiridigliotsky", "random_number":10, "date_time":"2011-09-17T14:33:22"}We will now write code to convert a contribution object to JSON (this is also called marshalling in computer science), and then write code to convert JSON back to a contribution object (unmarshalling).
The first step is to add command to_JSON in type contribution:
Edit file ty_contribution.osc
Add the following code after the declaration of the attributes:
command to_JSON
out result type:string end
endCommand to_JSON has no input arguments. It has one output argument of type string whose identifier is result.
To implement command to_JSON in factory contribution we could write a command similar to the to_string command in a previous chapter, like this:
command to_JSON
script
o_result = """{ "identifier":{{identifier.to_string}}, "first_name":"{{a_first_name}}", "last_name":"{{a_last_name}}", "random_number":{{a_random_number.to_string}}, "date_time":"{{a_date_time}}" }"""
end
endBut there is an easier way. Obix's standard library provides a simple service command to convert any object into JSON:
Edit file fa_contribution.osc
Add the following code after command to_string:
command to_JSON
script
o_result = se_JSON_converter.object_to_JSON ( this )
end
endse_JSON_converter.object_to_JSON uses the external Java library JSON_org.jar. To add this Java package to your project, please download the file and copy it into subdirectory random_arcs/work/java/lib/.
In the above code we use command object_to_JSON of service JSON_converter. This command takes an object of any type as input argument. In our case we provide the contribution object itself which is expressed by the keyword this. Internally this command uses Obix's reflection mechanism to get all attribute values and convert them into a JSON string.
The complete code for type and factory contribution now looks as follows (test script not shown):
type contribution
attribute identifier type:positive32 end
attribute first_name type:user_name end
attribute last_name type:user_name end
attribute random_number type:random_1_15 end
attribute date_time type:local_date_time end
command to_JSON
out result type:string end
end
endfactory contribution type:contribution
command to_string
script
o_result = """{{a_first_name}} {{a_last_name}} contributed '{{a_random_number.to_string}}' on {{a_date_time}}"""
end
end
command to_JSON
script
o_result = se_JSON_converter.object_to_JSON ( this )
end
end
creator create kind:in_all end
endWe should now test whether command to_JSON works correctly. In a previous chapter we created already a test script in factory contribution. This test script tests the factory as a whole. We could add a new test case to this script. A better way, however, is to attach a separate test script to the command script we just created, in order to keep related things closely together. Proceed as follows
Attach a test script to command to_JSON in factory contribution by inserting the test code as shown below:
command to_JSON
script
o_result = se_JSON_converter.object_to_JSON ( this )
end
test
script
// create a 'contribution' object and assign it to constant 'c'
const contribution c = fa_contribution.create ( &
identifier = 123 &
first_name = fa_user_name.create ( "Albert"~ ) &
last_name = fa_user_name.create ( "Newton"~ ) &
random_number = fa_random_1_15.create ( 5~ ) &
date_time = fa_local_date_time.create ( "2011-08-27T16:16:30"~ ) )
// execute command 'to_JSON' and store result in constant 'r'
const string r = c.to_JSON
// verify the result
verify r =v '''{"identifier":123,"first_name":"Albert","last_name":"Newton","random_number":5,"date_time":"2011-08-27T16:16:30"}'''
end
end
endExecute the system command compile_and_build again. Then re-execute run_tests and look at the test result shown in your system console.
The counterpart of converting an object to JSON is to convert JSON back to an object. Because this involves the creation of a new object we will use a creator in factory contribution to achieve this task.
The creator could be defined like this:
creator create_from_JSON
in JSON type:string end
out result type:contribution end
script
// code to create contribution object from JSON
end
endThis creator takes the JSON representation of a contribution object as input, and the script converts the JSON string to a contribution object which is returned in output argument result.
The above code would be fine if the JSON string provided as input for the creator was always a valid string. However, we can't assume this because the JSON string will come from an external source at runtime (e.g. a file), which means that it can be invalid and it can't be trusted.
This is a very typical example of a potential resource data error at runtime. We know that something can go wrong. Obix supports multiple output arguments and the solution in Obix is to provide a second output argument (typically named error) that reports any errors encountered.
Now there are two possible outcomes after the creator has been called:
The operation succeeded. In this case:
result contains the resulterror is void
The operation failed. In this case:
result is voiderror contains a description of the error
The creator's code now becomes:
creator create_from_JSON
in JSON type:string end
out result type:contribution voidable:yes end
out error type:string_to_object_conversion_error voidable:yes end
out_check check: i_result =r void xor i_error =r void end
script
// code to create contribution object from JSON
end
endNote the voidable:yes clause we have to add to both output arguments. We also use Contract Programming (Design by Contract) again (in the out_check instruction) to ensure that result or else error is void (exclusive or).
An interesting question appears: What should happen at runtime if the JSON string is invalid? The right answer is of course that the client code which calls the creator should check for any errors and take appropriate actions.
In practice, the same error handling is often appropriate for many or all clients calling the creator. For example, an error message is appended to a log file. In order to relieve all the clients from doing the same thing, the creator can take care of the error handling. However, flexibility is required, because different clients might need different error handling. The solution in Obix is to provide an error handler as input argument to the creator.
For this purpose Obix's standard library provides type system_error_handler which is defined as follows:
type system_error_handler
command handle_system_error
in error type:system_error end
end
endIn case of an error the creator will:
create an error object
call the error handler's handle_system_error command and pass it the error object
exit the script with the result output argument set to void and the error output argument containing the error object
Every client can provide its own specific error handler to the creator (including error handlers that do nothing). In practice, however, it is often appropriate to define a system wide error handler that is used by default if no specific error handler is provided. Therefore a global default error handler is defined in Obix's standard library. The path to it is org.obix.obix_core.system.se_system_utilities.a_default_system_error_handler. By default the global system error handler simply writes the error description to file error.log in the application's root directory. If you want to change this behavior you can create your own specific global error handler and assign it to se_system_utilities.a_default_system_error_handler in your application's initialization code.
As a result the code for the creator's additional input argument is:
in error_handler type:system_error_handler default:se_system_utilities.default_system_error_handler end
The final signature code for the creator is shown below:
creator create_from_JSON
in JSON type:string end
in error_handler type:system_error_handler default:se_system_utilities.default_system_error_handler end
out result type:contribution voidable:yes end
out error type:string_to_object_conversion_error voidable:yes end
out_check check: i_result =r void xor i_error =r void end
script
// code to create contribution object from JSON
end
endThe above code shows a very common pattern used in Obix whenever a command execution can fail at runtime. This pattern is used systematically in Obix's standard libraries and it is recommended to also use it in applications.
The benefit is that error handling now becomes easy and straightforward:
If you do nothing in your code then the default global system error handler will be used and write an error message to file error.log
If you want to change the default global error handling you can replace the default error handler with your own customized version and provide more sophisticated error handling. For example, you could log errors in another file or a database, or send email or SMS messages to administrators or developers, depending on the type of error that occurred.
If you need specific error handling in specific parts of your application then you can create your own appropriate error handlers and use them case by case in your code.
For more information please refer to Chapter 11, Runtime error handling
In order to reduce the size of code and avoid code duplication, different source code templates are available in the standard library. In our case template system_error_handler_input_argument can be used as a shortcut to define the error_handler input argument, and template result_xor_string_to_object_conversion_error_output can be used to simplify the code for the output arguments. The creator's simplified code becomes:
creator create_from_JSON
in JSON type:string end
%system_error_handler_input_argument
%result_xor_string_to_object_conversion_error_output < result_type: contribution >
script
// code to create contribution object from JSON
end
endWhat remains to be done is to write the script code to convert the JSON string into a contribution object.
The simplest way to do this is to use se_JSON_converter again. Command JSON_to_object in this service is defined as follows:
command JSON_to_object in JSON type:string end in type_of_object type:type end %system_error_handler_input_argument %result_xor_system_error_output < any_type > end
This command takes three input arguments:
The first input argument (JSON) is a string that holds the JSON representation of the object.
The second input argument (type_of_object) defines which type of object has to be created from JSON.
Executing this command obviously fails if the JSON input string is invalid. Therefore the command also has an error_handler input argument whose purpose is the same as explained previously for creator create_from_JSON
Because JSON_to_object can fail, there are also two output arguments. The pattern we used previously for creator create_from_JSON's output is also used here:
Output argument result returns the object created from JSON if no error occurs. Note that the type of result is any_type, because this is a general command that can be used for any type.
Output argument error contains an error object if anything goes wrong.
The only thing to do in creator create_from_JSON is to call the above command. Insert the following code in file fa_contribution.osc, just after the existing creator:
creator create_from_JSON
in JSON type:string end
%system_error_handler_input_argument
%result_xor_system_error_output < contribution >
script
var any_type object_from_JSON
se_JSON_converter.JSON_to_object ( &
JSON = i_JSON &
type_of_object = se_reflection.library_repository.type_by_unprefixed_id_string ( "contribution" ) &
error_handler = i_error_handler ) &
( v_object_from_JSON = result &
o_error = error )
if o_error =r void then
// cast 'v_object_from_JSON' (which is declared of type 'any_type') to type 'contribution'
// 'type_check:no' tells the compiler to skip type checking at compile-time
o_result = v_object_from_JSON type_check:no
else
o_result = void
end if
end
endDoes everything work correctly? To find it out, insert the following test script just before the last end instruction in creator create_from_JSON:
test
script
test i_JSON = """{ "identifier":123, "first_name":"Albert", "last_name":"Newton", "random_number":5, "date_time":"2011-08-27T16:16:30" }"""
verify v_result.identifier =v 123
verify v_result.first_name.value =v "Albert"~
verify v_result.last_name.value =v "Newton"~
verify v_result.random_number.value =v 5~
verify v_result.date_time.value =v "2011-08-27T16:16:30"~
// test with invalid JSON (first brace removed)
// remark: 'error_handler' is set to 'se_system_utilities.do_nothing_system_error_handler' in order to
// display only test results on the system console
test i_JSON = """ "identifier":123, "first_name":"Albert", "last_name":"Newton", "random_number":5, "date_time":"2011-08-27T16:16:30" }""" &
error_handler = se_system_utilities.do_nothing_system_error_handler
verify v_result =r void
verify v_error #r void
// test with invalid attribute name ('ident' instead of 'identifier')
test i_JSON = """{ "ident":123, "first_name":"Albert", "last_name":"Newton", "random_number":5, "date_time":"2011-08-27T16:16:30" }""" &
error_handler = se_system_utilities.do_nothing_system_error_handler
verify v_result =r void
verify v_error #r void
// test with invalid attribute value ('first_name' contains invalid characters)
test i_JSON = """{ "identifier":123, "first_name":"Albert<>", "last_name":"Newton", "random_number":5, "date_time":"2011-08-27T16:16:30" }""" &
error_handler = se_system_utilities.do_nothing_system_error_handler
verify v_result =r void
verify v_error #r void
end
endRun the system commands compile_and_build and run_tests, and look at the test results.
![]() | Note |
|---|---|
If you are tired of manually executing the system command compile_and_build each time before excuting run_tests then you can create your own system command to automatically call compile_and_build and then run_tests. In Windows, for example, you can create compile_build_and_test.bat with the following content:
@echo off call compile_and_build if errorlevel 1 goto end call run_tests :end |