📜 ⬆️ ⬇️

REST server and thin client using vibe-d

Good day, Habr! If you want to share your application on the server and client, if you want to add an API to your vibe-site or if you simply have nothing to do.

These situations are not much different, so we first consider the simple case:


So, what we need to do to make 2 - rest-server and thin client from one normal application:


Boring but important points
First, a little about the model. At the time of writing, vibe-d-0.7.30-beta.1 did not support function overloading (in general), which is partly logical, since we would try to call the method without having accurate information about the arguments, because we transmit them over the network, vibe I would not even know what type to bring them in - it would be necessary to find out by brute force, but there are some subtle points (“5” can be reduced to both int and float, for example).

In addition, the arguments and returned data of the methods should be able to [de] be serialized using vibe.data.json. All built-in data types and simple structures (with no private fields) can do this. To implement [de] serialization, you can declare 2 methods static MyType fromJson(Json data) and Json toJson() const , which describes the process of converting complex structures into Json type, an example .

This does not apply to returned interfaces, they also work through the transmission of arguments over the network, but there is another point: the method that returns an instance of the class that implements the returned interface object should not take arguments. Here you can explain only one thing: an instance is used to register the rest-interface, and if the function takes arguments, then it is possible to create an instance with arguments that have init-values, but you need to create something to register the nested interface.

So select the interface:

 interface IModel { @method(HTTPMethod.GET) float triangleAreaByLengths(float a, float b, float c); @method(HTTPMethod.GET) float triangleAreaByPoints(Point a, Point b, Point c); } class Model : IModel { ... } 

The @method(HTTPMethod.GET) decorators @method(HTTPMethod.GET) required for building routing. There is also a way to do without them - use the naming convention of methods (prefixes):


Server code will be recorded according to the classic vibe in the static module constructor:

 shared static this() { auto router = new URLRouter; router.registerRestInterface(new Model); //     auto set = new HTTPServerSettings; set.port = 8080; set.bindAddresses = ["127.0.0.1"]; listenHTTP(set, router); } 

Finally, the changes in the code using the model:

 ... auto m = new RestInterfaceClient!IModel("http://127.0.0.1:8080/"); //       ... 

The framework itself implements calls to the server and [de] serialization of data types.

As a result, we divided the application onto the server and the client with minimal changes to the existing code! By the way, thrown exceptions are thrown by vibe into the client application, unfortunately, without saving the type of exception.

Consider a more complex case - the model has methods that return arrays of non-serializable objects (classes). There is no need to do without changing the existing code here. We realize this situation in our example.

We will return different point aggregators:

 interface IPointCalculator { struct CollectionIndices { string _name; } //      @method(HTTPMethod.GET) Point calc(string _name, Point[] points...); } interface IModel { ... @method(HTTPMethod.GET) Collection!IPointCalculator calculator(); } class PointCalculator : IPointCalculator { Point calc(string _name, Point[] points...) { import std.algorithm; if (_name == "center") { auto n = points.length; float cx = points.map!"ax".sum / n; float cy = points.map!"ay".sum / n; return Point(cx, cy); } else if (_name == "left") return points.fold!((a,b)=>ax<bx?a:b); else throw new Exception("Unknown calculator '" ~ _name ~ "'"); } } class Model : IModel { PointCalculator m_pcalc; this() { m_pcalc = new PointCalculator; } ... Collection!IPointCalculator calculator() { return Collection!IPointCalculator(m_pcalc); } } 

In fact, the IPointCalculator is not a collection element, but the collection itself and the CollectionIndices structure just indicates the presence of indices used to obtain the elements of this collection. The underscore before _name determines the format of the request to the calc method as calculator/:name/calc , where :name then passed as the first parameter to the method, and CollectionIndices allows you to build such a query when implementing the interface using a new RestInterfaceClient!IModel .

It is used like this:

 ... writeln(m.calculator["center"].calc(a, b, c)); ... 

If the return type is changed from Collection!IPointCalculator to IPointCalculator then little will change:

 ... writeln(m.calculator.calc("center", a, b, c)); ... 

In this case, the format of the request will remain the same. The role of the Collection in this combination is not entirely clear.

For a snack, we sell our client’s web version. For this you need:


The diet template used in the vibe is very similar to the jade :

 html head title  REST style. .label { display: inline-block; width: 20px; } input { width: 100px; } script(src = "model.js") script. function getPoints() { var ax = parseFloat(document.getElementById('ax').value); var ay = parseFloat(document.getElementById('ay').value); var bx = parseFloat(document.getElementById('bx').value); var by = parseFloat(document.getElementById('by').value); var cx = parseFloat(document.getElementById('cx').value); var cy = parseFloat(document.getElementById('cy').value); return [{x:ax, y:ay}, {x:bx, y:by}, {x:cx, y:cy}]; } function calcTriangleArea() { var p = getPoints(); IModel.triangleAreaByPoints(p[0], p[1], p[2], function(r) { document.getElementById('area').innerHTML = r; }); } body h1    div div.label A: input#ax(placehoder="ax",value="1") input#ay(placehoder="ay",value="2") div div.label B: input#bx(placehoder="bx",value="2") input#by(placehoder="by",value="1") div div.label C: input#cx(placehoder="cx",value="0") input#cy(placehoder="cy",value="0") div button(onclick="calcTriangleArea()")  p : span#area 

It looks, of course, so-so, but for an example of norms:


Changes in server code:

 ... auto restset = new RestInterfaceSettings; restset.baseURL = URL("http://127.0.0.1:8080/"); router.get("/model.js", serveRestJSClient!IModel(restset)); router.get("/", staticTemplate!"index.dt"); ... 

As we can see, the vibe for us generates js-code to access our API.

In conclusion, it can be noted that at this stage there are some rough edges, for example, incorrect generation of js code for all returned interfaces (forgot to add this. For these fields in the js object) and for collections in particular (incorrect generation of url - :name doesn’t matter replaced). But these roughnesses are easily repairable, I think they will be fixed in the near future .

That's all! The sample code can be downloaded on github .

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


All Articles