#Client design
Typescript and Java clients generatad by API Workbench can be roughly viewed as consisting of four parts:
- API independent execution core.
- Model, reflecting API structure, deligating calls to the core.
- API independent authorization and validation services prompted by the core.
- API dependent authorization and validation databases prompted by the services.
With client designed this way a call decomposes on following steps:
- The Model passes call information to the Core. This info must contain a method key of some kind which will be used to access services.
- The Core passes the key to the Services in order to obtain ways to validate and authorize the particular method.
- Once the ways are obtained, the Core performs authorization and validation.
- The call is executed and response obtained.
- The response is validated. (This step is intended to check RAML source correctness)
- The Core constructs response object and passes it to the Model.
###Typescript client Let's take a look how a variation of this process works in the Typescript client:
-
The model registers the
RamlWrapper.Api
instance in theAuthenticationManager
(viaExecutionEnvironment
). For the yet unknowRamlWrapper.Api
theAuthenticationManager
determines correspondence betweenRamlWrapper.Method
s andRamlWrapper.SecurityScheme
s. -
APIExecutor.execute()
recieives call information which consists of:
- complete call URL
- method name
- (optional) payload object
- (optional) options objects (containing query and header parameters)
- canonicalMethodUri (complete method URI)
-
The
APIExecutor
utilizes the call info in order to buildharExecutor.ExtendedHarRequest
which is passed toAuthenticationDecorator
. In factharExecutor.ExtendedHarRequest
is exactlyHar.Request
+canonicalMethodUri
. -
AuthenticationDecorator
asks theAuthenticationManager
to insert authorization parameters intoHAR.Request
. For this purpose it usescanonicalMethodUri
to findRamlWrapper.Method
inside contextRamlWrapper.Api
and passes thisRamlWrapper.Method
to theAuthenticationManager
along withHAR.Raquest
. -
After being authorized, the
HAR.Request
is passed toValidatingExecutor
. It obtainsRamlWrapper.Method
just the same way asAuthenticationDecorator
, takes RAML types of its bodies (if any) and validates request agains them. -
The
HAR.Request
is passed toSimpleExecutor
which delegates it toXmlHttpRequest
module. -
The response obtained is wrapped to
HAR.Response
and passed back toValidatingExecutor
. -
The
ValidatingExecutor
validates response againts RAML types of response obtained from theRamlWrapper.Method
and passes it back toAPIExecutor
(viaAuthenticationDecorator
which does nothing on the way back). -
The
APIExecutor
usesHAR.Raquest
to construct response object and returns this object to the Model.
#Client generator design Thus, implementing a client generator can be separated to
- Designing protocol of comunication between model and core.
- Implementing execution core
- Implementing a model generator
- Implementing services core
- Implementing services database generator
###Model generator It may appear not handy to generate client model code directly from the parsed RAML. We suggest deviding this process on two steps:
- Iterate throw the RAML AST and build some abstract model.
- Serialize the gathered model into code on particular language.
API Workbench project provides a simplified Typescript code model: TSDeclModel
Let's consider an example of gathering client model. Assume that we have following require statements in our exampl module:
import TS = require('{some path to src/ramlscript/TSDeclModel}')
import raml2ts1 = require('{some path to src/ramlscript/raml2ts1}')
import RamlWrapper = require('{some path to src/raml1/artifacts/raml003Parser}')
import norebookModelBuilder = require('{some path to src/ramlscript/notebookModelBuilder}')
#####Parse API and create a TS.TSModule
:
var api = norebookModelBuilder.loadApi1('{path to your RAML spec root .raml file}')
var module = new TS.TSModule();
#####Create a model tree isomorphic to API structure:
We need TSModelDecl
classes for representing classis and their members.
-
TS.TSInterface
is aTSModelDecl
class which is used to define interfaces or classes (also enums and function interfaces). In our example for eachRamlWrapper.Resource
we will create aTS.TSInterface
successor:raml2ts1.ResourceMappedInterface
. The difference between these two is that withraml2ts1.ResourceMappedInterface
you are able to retrieve originalRamlWrapper.Resource
which may appear useful on serialization step:var resource:RamlWrapper.Resource = resourceMappedInterfaceInstance.original().originalResource;
-
TS.TSAPIElementDeclaration
is aTSModelDecl
class which is used to define interface or class members -- both fields and methods. In our example for each childRamlWrapper.Resource
(which belong to RamlWrapper.Api or another RamlWrapper.Resource) we will create a member (inside class corresponding to its parent) represented byTS.TSAPIElementDeclaration
successeor:raml2ts1.TSResourceMappedApiElement
. The reason to useraml2ts1.TSResourceMappedApiElement
rather thenTS.TSAPIElementDeclaration
is just the same: withraml2ts1.TSResourceMappedApiElement
you are able to retrieve originalRamlWrapper.Resource
:var resource:RamlWrapper.Resource = tsResourceMappedApiElementInstance.originalResource;
processResource(resource:RamlWrapper.Resource,parent:TS.TSInterface){
var relUri = resource.relativeUri().value();
//generate name for member corresponding the resource in some way
var memberName = constructFieldName(relUri);
//here we add member to the parent class. On level 1 of recursion it is `apiClass`
var member = new raml2ts1.TSResourceMappedApiElement(parent,memberName,resource);
//generate name for class corresponding the resource in some way
var className = constructClassName(relUri);
//here we create class corresponding to the resource. It is passed to next level of recursion.
var resourceClass = new raml2ts1.ResourceMappedInterface(module,className,member);
member.rangeType = resourceClass.toReference();
resource.resources.forEach(x=>processResource(x,resourceClass));
}
var apiClass = new TS.TSInterface(module,'Api');
api.resources.forEach(x=>processResource(x,apiClass));
#####Serialize model
Now the module is capable of returning classes corresponding to API and resources, which, in turn, can return a list of their members. Here is a dummy example of model serialization:
function serializeClass(clazz:TS.TSInterface){
var name = clazz.name();
var content = 'export class ' + name + '{\n\n';
clazz.children().forEach(x => content += serializeMember(x));
content += '}';
//some way to store obtained class code
write(content,class);
}
function serializeMember(member:TS.TSAPIElementDeclaration){
var name = member.name();
//retrieve members class
var memberClass:raml2ts1.ResourceMappedInterface
= (<raml2ts.ResourceMappedReference>mamber.rangeType).resourceInterface();
var className = memberClass.name();
return ' ' + name + ':' + 'className' + ';\n\n';
}
module.children().forEach(x=>serializeClass(x));
#####Further code model enriching
The example above is too simple and can not be considered a sufficient client model. For sufficient example you may refer to the codebase. The entry point is raml2ts1.createApiModule()
. It calls the raml2ts1.ShortStyleModelBuilder.raml2TSModel()
method where we start processing RamlWrapper.Api
.
Another key methods of ShortStyleModelBuilder
are:
processChildResources()
Here we handle those brother resources which differ bymediaTypeExtention
ormediaTypeSuffix
URI parameters. Sometimes it happens that we have the following situation in our RAML sspec:
/parentResource
/childResource{mediaTypeExtension}
#some methods here
/childResource
#no methods here
/grandChildResource1
/grandChildResource2
Formally, here we have two different child resources and for parent class we can create two different fields corresponding them. But rather then create two fields, we create one for /childResource{mediaTypeExtension}
and attach to it children of /childResource
.
-
processResource()
Here we process URI parameters of resources: those members which correspond resources with query parameters become methods. Also we initiate methods processing. -
customizeMethod()
Here we createTS.APIElementDeclaration
successor (raml2ts1.TSFullyMappedApiElement
) corresponding the method (The goal of using this successor is again the same: it provides access to originalRamlWrapper.Method
). Then we create types for method bodies (form, JSON or XML), response bodies (JSON or XML) and options (query and header parameters together).
#####Example of serializing sufficient model Serializing Java client is a good example: see 'src/ramlscript/JavaClientSerializer'