Style dart is a backend framework written in Flutter coding style.
You can see our main motivation, purpose and what it looks like in general with this article. Documentation and website will be developed soon.
style_dart: latest
Similar to Flutter, "everything is a component" structure used in Style.
The runService(MyComponent()) method runs a style application.
There is a component to create a simple server : Server
It is the equivalent of "MaterialApp" or "CupertinoApp" in Flutter.
Server is a ServiceOwner. ServiceOwner holds states, cron jobs and component tree.
Not Implemented yet but components such as MicroService and ServiceServer(It will be used to share services such as
Logger between Servers/Microservices.) are ServiceOwner.
Server receive and hold main gateway(children) , default services(like httpService) , root
endpoint (rootEndpoint) , exception endpoints and etc.
Now let's create a simple http server:
class MyServer extends StatelessComponent {
const MyServer({Key? key}) : super(key: key);
@override
Component build(BuildContext context) {
return Server(
/// default localhost:80
httpService: DefaultHttpServiceHandler(
host: "localhost",
port: 8080
),
children: [
// main gateway
]);
}
}void main() {
runService(MyServer());
}Now, server is ready from localhost but we have to define endpoints.
Endpoints create functions where client requests are fulfilled.
"Endpoint" is a component. It can be put in Component parameters. But all ends in the component tree must end with Endpoint and Endpoints do not have child/children.
Boxes are Component, circles are Endpoint.
Now let's create an Endpoint that response with "Hello World!".
class HelloEndpoint extends Endpoint {
@override
FutureOr<Object> onCall(Request request) {
return "Hello World!";
}
}The main Gateway that handles requests is the Server's children value.
There must be a Route between all Endpoint in the component tree and the Gateway above them.
Otherwise, you will get an error while the server is being built.
Now let's place the Endpoint to respond to the "http://localhost/hello" request.
class MyServer extends StatelessComponent {
const MyServer({Key? key}) : super(key: key);
@override
Component build(BuildContext context) {
return Server(
httpService: DefaultHttpServiceHandler(
host: "localhost",
port: 8080
),
children: [
Route("hello", root: HelloEndpoint())
]);
}
}Route takes a root and a child.
root handles requests ending with the entered path segment.
In this example HelloEndpoint handles request "http://localhost/hello"
If we want to process "hello/*" and its sub-routes, the child parameter must be defined.
Also, if we want to handle "hello/*" and its sub-routes with HelloEndpoint, handleUnknownAsRoot must be true.
Now start server with dart run or your IDE's run command.
And call "http://localhost/hello" .
I named the Component, which creates functions to be used as middleware, Gate.
Gate's onRequest function works with a request and waits for a request or response.
If the return value is Request, the request continues. It can be used to manipulate the content of requests.
If the return value is a Response, the response is sent to the client.
Also, if you want to send an error message, the exceptions thrown in this function are handled by the
context's ExceptionWrapper.
Gate in the example only works for "host/api/users/*"
The second gate in the example, AuthFilterGate, which is a Gate implementation, optionally only accepts auth users.
Since Style has a modular structure, it will have many ready-made Components that the developers will contribute.
The following example also has Gateway and argument path segment ("name") usage example.
class MyServer extends StatelessComponent {
const MyServer({Key? key}) : super(key: key);
@override
Component build(BuildContext context) {
return Server(
children: [
// host/api/
Route(
"api",
child: Gateway(
children: [
/// GATE IS HERE :)
Gate(
onRequest: (request) {
// DO SOMETHING
return request; // return Request or Response
},
// host/api/users/
child: Route("users",
// host/api/users/{name}
child: Route("{name}",
root: SimpleEndpoint((req, c) {
// [Create] request handled with
// this context's DataAccess
return Create(
collection: "greeters",
data: {"greeter": req.arguments["name"]});
}))),
),
// host/api/posts
Route("posts",
root: AuthFilterGate(
child: SimpleEndpoint.static("Hi")
))
]
))
]);
}
}The first Gate in the example handles both the root segment and all subsegments of "/api/users".
In the second example, it only handles the root segment of "api/posts".
You can customize the endpoints that handle the exceptions.
It is possible to customize with exact types(e.g. in example bellow) or with super types(e.g. ClientError or Exception). Applies to all sub-components unless re-wrapped.
When determining the endpoint to handle exceptions, they are checked in the following order. exact -> (if is implementation) super (e.g. ClientError) -> Exception
class MyStyleExEndpoint extends ExceptionEndpoint<BadRequest> {
MyStyleExEndpoint() : super();
@override
FutureOr<Object> onError(Message message, BadRequest exception, StackTrace stackTrace) {
return "Will always be bad !!!";
}
}
//TODO: Add the component your Component tree.
Component getExceptionWrapper() {
return ExceptionWrapper<BadRequest>(
child: Route("always_throw", root: Throw(BadRequest())),
exceptionEndpoint: MyExceptionEndpoint());
}Throw is an endpoint that always sends an exception
Accessing data is required for a backend application.
In Style, there are structures that I call base service.
DataAccess , HttpService , Logger , Authorization , Crypto and WebSocketService are currently available
services.
Each service has its own functions.
All the services can be assigned as the default service of the Server.
class MyServer extends StatelessComponent {
const MyServer({Key? key}) : super(key: key);
@override
Component build(BuildContext context) {
return Server(
httpService: DefaultHttpServiceHandler(),
webSocketService: StyleTicketBaseWebSocketService(),
logger: MyLogger(),
dataAccess: DataAccess(MongoDbDataAccessImplementation()),
children: [
Route("hello", root: HelloEndpoint())
]);
}
}MongoDb implementation available with style_mongo package
You can use a different service for part of your Component tree. Wraps are valid as long as they are not re-wrapped.
class MyServer extends StatelessComponent {
const MyServer({Key? key}) : super(key: key);
@override
Component build(BuildContext context) {
return Server(
// server default
dataAccess: DataAccess(MongoDbDataAccessImplementation()),
children: [
Route("hello", root: HelloEndpoint()),
// Use different service for
// "/other/*"
ServiceWrapper<DataAccess>(
child: Route("other", root: HelloEndpoint()),
service: DataAccess(SimpleCacheDataAccess())
),
]);
}
}You can access all services this way :
DataAccess.of(context)
There are many ways you can access your data. One is to return an Event in the endpoint onCall function.
You can also access functions directly : DataAccess.of(context).read(...) .
The following endpoint is put on the "/greet/{name}" route.
Create greeter by name
class HelloEndpoint extends Endpoint {
@override
FutureOr<Object> onCall(Request request) {
return Create(
collection: "greeters",
data: {"greeter": request.arguments["name"]});
}
}Read All Greeters
class GreetersEndpoint extends Endpoint {
@override
FutureOr<Object> onCall(Request request) {
return ReadMultiple(
collection: "greeters");
}
}You can handle all data operations with a single endpoint.
You can process this completely yourself, or you can use ready-made Components.
AccessPoint take a callback that runs with request and returns. This AccessEvent is handled by the
context's DataAccess.
class MyServer extends StatelessComponent {
const MyServer({Key? key}) : super(key: key);
@override
Component build(BuildContext context) {
return Server(
httpService: DefaultHttpServiceHandler(),
children: [
Route("col",
// AccessEvent
root: AccessPoint((req, ctx) {
return AccessEvent(
access: Access(
type: _getAccessType(req),
collection: req.arguments["col"],
///optional parameters
//query
//identifier
//data
//pipeline
//settings
),
request: req);
}))
]);
}
}Ready-made access points, both in the core package and developed by the developers, can be used.
RestAccessPoint("route")
It is an AccessPoint that works according to Rest API standards. RestAccessPoint documentation is being prepared.
base package
Command line app for debugging, monitoring, templating.
Test framework for style
Cron job definer and executor.
Similar to Microservice architecture, you can use to base services (logger, crypto, data access, etc.) from remote servers.
Services can be shared between servers(or microservices).
Monitoring app shows server status , state properties, usage statistics , logs, component tree etc. The monitoring app will be hosted by styledart.dev. It will also be available as open source.
Many run/test options can be set by cli app. Also, cli app will be used for monitoring app.