📜 ⬆️ ⬇️

Experience of using contracts in REST API calls


There are two irreconcilable camps of software developers: the first one says that the more an application crashes, the better it works. The second is that the programmer is smart enough to handle any abnormal situation. A characteristic feature of the former is the abundance of Asset directives in the code, while the latter, even addition operations, are placed in a try - catch block. Moreover, both camps call this kind of approach "contract programming". The arguments of the first are reduced to an article in Wikipedia , the arguments of the second - to the book Feel the Class by Bertrand Meyer .

As part of a scientific study, it would be correct to consider the whole variety of approaches to the protective mechanisms of programming, especially those in the title of this article, however, I would like to demonstrate only one of the possibilities that the second camp has.

The main message is this: If an exception arises in an application, then we pretend that the operation that led to it did not cause it at all. Well, in any case, it will look like this from the point of view of the product user. In addition, we add an important limitation here - we are talking exclusively about client-server interaction .

The practical aspect is this: When a REST response comes from the server that we cannot process due to data integrity problems (values ​​have wrong ranges, no required fields, etc.), we simply ignore the call and don’t try to parse it.
')
For the sake of fairness, it must be said that there are libraries that produce document-object transformations on the resulting JSON / XML object. The downside of their medal is that, as a rule, such an approach makes the use of the CoreData model unviable, since two model logics are required: a document-object transformation (JSON -> Binary Objects) and an object-relational transformation (Binary Objects -> CoreData Entities), support both logic, and do synchronization between them - I do not want anyone.

The usual client-server interaction process is as follows:
  1. We form a request to the server with the available parameters (request)
  2. Make a request for a given url
  3. We get a response
  4. We retrieve from the answer data (parsing)


As a rule in the process of parsing we are waiting for surprises, because not everything that comes from the server is trustworthy. You have to check each parameter for type matching, band ownership, key validity, etc., and this significantly increases the code of the parsing method, especially if the resulting hierarchical structure has many nesting levels and different data types (arrays, dictionaries, etc.) validating each of the parameters suggests the logic of validation at least in a separate method. This will make the approach somewhat more flexible:
We get a response. Validate the response. If the validation was successful, we do the parsing, otherwise we do nothing (or issue a notification to the server / user).
It’s no secret that JSON is at the heart of the REST interaction. Those who prefer to use XML, as a rule, have their own mechanisms for solving similar problems. For example, WCF controls types at the stage of creating proxy classes. Alas, JSON users of this sugar are deprived, and everything has to be done manually. As a result, the code for checking the validity of an object often becomes as big as the parsing code.

Help in solving this situation allows the use of the mechanism JSON schemes. The format is very well standardized and has a redundant description: json-schema.org , in addition, there are many online tools that allow you to create schemes according to the entered JSON: jsonschema.net/#

Let's try to consider a practical example for the programming language Swift.
In a cursory search, we managed to find a public service that returns a JSON response to a simple GET request: httpbin.org/get?myFirstParam=myFirstValue&mySecondParam=MySecondValue

The answer will be roughly as follows:
Response
{
"args": {
"myFirstParam": "myFirstValue",
"mySecondParam": "MySecondValue"
},
"headers": {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3",
"Host": "httpbin.org",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:45.0) Gecko/20100101 Firefox/45.0"
},
"origin": "193.105.7.55",
"url": "https://httpbin.org/get?myFirstParam=myFirstValue&mySecondParam=MySecondValue"
}



The answer does not contain any practically useful information, but allows you to debug the process of interaction. The response contains the string GET request, and the parameters that were passed, as well as some information about the browser through which the request was made. When making a request from a simulator or a real device, the result of the response may be slightly different. At the same time, it must be subordinated to a certain scheme, which can be extracted using online tools (http://jsonschema.net/#/ and similar).

In the left pane, install all the checkboxes. I recommend setting the “Arrays” option switch to “Single schema (list validation)” (Swift language features).


Copy the browser response in the upper left window, and make sure that we have placed valid JSON. This will be clear from the words “Well done! You provided valid JSON. ”On a green background directly below the window. Unfortunately, the response to the XCode console, even with the print () operator, does not comply with the format requirements. If you still decide to take the answer text from the console, you will have to replace all the equals “=” with a colon “:”, and take all the field names in double quotes.


After clicking on the Generate Schema button, we get in the right window a rather long scheme for such a small query:

Scheme
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "http://myjsonschema.net",
"type": "object",
"additionalProperties": true,
"title": "Root schema.",
"description": "An explanation about the purpose of this instance described by this schema.",
"name": "/",
"properties": {
"args": {
"id": "http://myjsonschema.net/args",
"type": "object",
"additionalProperties": true,
"title": "Args schema.",
"description": "An explanation about the purpose of this instance described by this schema.",
"name": "args",
"properties": {}
},
"headers": {
"id": "http://myjsonschema.net/headers",
"type": "object",
"additionalProperties": true,
"title": "Headers schema.",
"description": "An explanation about the purpose of this instance described by this schema.",
"name": "headers",
"properties": {
"Accept": {
"id": "http://myjsonschema.net/headers/Accept",
"type": "string",
"minLength": 1,
"title": "Accept schema.",
"description": "An explanation about the purpose of this instance described by this schema.",
"name": "Accept",
"default": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"enum": [
null,
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
]
},
"Accept-Encoding": {
"id": "http://myjsonschema.net/headers/Accept-Encoding",
"type": "string",
"minLength": 1,
"title": "Accept-Encoding schema.",
"description": "An explanation about the purpose of this instance described by this schema.",
"name": "Accept-Encoding",
"default": "gzip, deflate, br",
"enum": [
null,
"gzip, deflate, br"
]
},
"Accept-Language": {
"id": "http://myjsonschema.net/headers/Accept-Language",
"type": "string",
"minLength": 1,
"title": "Accept-Language schema.",
"description": "An explanation about the purpose of this instance described by this schema.",
"name": "Accept-Language",
"default": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3",
"enum": [
null,
"ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3"
]
},
"Host": {
"id": "http://myjsonschema.net/headers/Host",
"type": "string",
"minLength": 1,
"title": "Host schema.",
"description": "An explanation about the purpose of this instance described by this schema.",
"name": "Host",
"default": "httpbin.org",
"enum": [
null,
"httpbin.org"
]
},
"User-Agent": {
"id": "http://myjsonschema.net/headers/User-Agent",
"type": "string",
"minLength": 1,
"title": "User-Agent schema.",
"description": "An explanation about the purpose of this instance described by this schema.",
"name": "User-Agent",
"default": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:45.0) Gecko/20100101 Firefox/45.0",
"enum": [
null,
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:45.0) Gecko/20100101 Firefox/45.0"
]
}
}
},
"origin": {
"id": "http://myjsonschema.net/origin",
"type": "string",
"minLength": 1,
"title": "Origin schema.",
"description": "An explanation about the purpose of this instance described by this schema.",
"name": "origin",
"default": "193.105.7.55",
"enum": [
null,
"193.105.7.55"
]
},
"url": {
"id": "http://myjsonschema.net/url",
"type": "string",
"minLength": 1,
"title": "Url schema.",
"description": "An explanation about the purpose of this instance described by this schema.",
"name": "url",
"default": "https://httpbin.org/get",
"enum": [
null,
"https://httpbin.org/get"
]
}
},
"required": [
"args",
"headers",
"origin",
"url"
]
}



In principle, the scheme can be reduced without setting a tick, and setting the “Array” switch to the “Single empty schema” state, but this way we will lose the opportunity to use some of the common features of the scheme and Swift language sharing.

In real life, the server’s response is often quite large, and contains many array elements that can also be dictionaries. In this case, the scheme will not increase in size, since the constructor will correctly process the repeatability of elements.

Create a file called response.json and add it to the project.
If you use Cocoapods add the line
pod 'VVJSONSchemaValidation' in your Podfile . If you don’t use Cocoapods, you’ll have to go directly to the Vita Voloshin GitHub repository: github.com/vlas-voloshin/JSONSchemaValidation

After updating Cocoapods, it will be enough to add the following class to the project source code:

Validator class
import UIKit
import VVJSONSchemaValidation

class Validator
{

private var _schemaName = ""
var schemaName:String {
get {
return _schemaName
}
set(value)
{
_schemaName = value
guard let path = NSBundle.mainBundle().pathForResource(value, ofType: "json") else {
return
}

do
{
if let schemaData = NSData(contentsOfFile:path) {
self.schema = try VVJSONSchema(data: schemaData, baseURI: nil, referenceStorage: nil)
}
}
catch let error as NSError
{
print("\n")
print("===============================================================")
print("Schema '\(value).json' didn't create:\n\(error.localizedDescription)")
print("===============================================================")
print("\n")
}
}
}

private var schema:VVJSONSchema?

func validate(response:AnyObject?) -> Bool
{
if let schema = self.schema
{
do {
try schema.validateObject(response!)
}
catch let error as NSError
{
print("\n")
print("===============================================================")
print("\(error.userInfo["NSLocalizedDescription"]!)\n\(error.userInfo["NSLocalizedFailureReason"]!)")
print("===============================================================")
print("\n")
return false
}
}

return true
}
}




In the class in which you receive a response from the server, add:

let validator = Validator()
validator.schemaName = "response"


And in the method (block) where we get the server response, we write:

if self.validator.validate(response) {
self.parse(response) // <— JSON
}


That's all.
Now, if from the server side the data that does not correspond to the specified scheme comes, then the parsing mechanism will not be launched. You do not need to describe the logic of the JSON response in the code, only to understand whether there is some kind of mistake. That is, if it is correct, you can safely parse. Of course, such a code does not protect you 100%, but 99.9% of erroneous answers will be eliminated. Experience shows that with manual programming of logic, the number of erroneous answers leading to the crash of the system is eliminated only in 68.2%.

Additional buns from this approach can highlight the fact that you can specify default values ​​directly in the scheme:
"default": «193.105.7.55" "default": "127.0.0.1",


And in “enum” provide a list of those values ​​that are valid for the data model object. In my case, this is an Optional String (String?), I.e., a string that could potentially contain either nil or "193.105.7.55":
Enum
"enum": [
null,
"193.105.7.55"
]



It is very easy to proceed to the development of this concept:
  1. Schemes can be created by developers who are developing a REST API, and can be integrated into an application as a finished product. In case of errors, there will always be one responsible for data integrity violations.
  2. In case data validation fails, the name of the scheme, request and response of the server are transferred to the server side. This will allow you to quickly track and adjust the work of the API on the back-end side

Source: https://habr.com/ru/post/283012/


All Articles