Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Springboot vs Ballerina comparison sample code #29

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions rest-social-media/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Springboot and Ballerina

A sample code base which touches key features of each technology. The sample is based on a simple API written for a social-media site which has users and associated posts. Following is the high level component diagram.

<img src="springboot-and-ballerina.png" alt="drawing" width='500'/>

Following are the features used for the implementation

1. Configuring verbs and URLs
2. Error handlers for sending customized error messages
3. Adding constraints/validations
4. OpenAPI specification for Generating API docs
5. Accessing database
6. Configurability
7. HTTP client
8. Resiliency - Retry
9. Docker image generation

# Setting up each environment

## Spring boot
Run the `springboot-docker-compose.yml` docker compose setup.
```sh
docker compose -f springboot-docker-compose.yml up
```

## Spring boot (Reactive)
Run the `springboot-reactive-docker-compose.yml` docker compose setup.
```sh
docker compose -f springboot-reactive-docker-compose.yml up
```

## Ballerina
Run the `ballerina-docker-compose.yml` docker compose setup.
```sh
docker compose -f ballerina-docker-compose.yml up
```

# Try out
## Spring boot (Default and Reactive)
- To send request open `springboot-social-media.http` file using VS Code with `REST Client` extension

## Ballerina
- To send request open `ballerina-social-media.http` file using VS Code with `REST Client` extension

36 changes: 36 additions & 0 deletions rest-social-media/ballerina-docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
version: '2.14.0'

services:
social-media:
image: 'integrationsamples/ballerina-social-media:0.0.1'
ports:
- '9090:9090'
depends_on:
sentiment-analysis:
condition: service_started
mysql:
condition: service_healthy
network_mode: "host"

sentiment-analysis:
image: 'shafreen/ballerina-sentiment-api:0.0.1'
ports:
- '9099:9099'
network_mode: "host"

mysql:
image: 'mysql:8-oracle'
ports:
- '3306:3306'
network_mode: "host"
environment:
- MYSQL_ROOT_PASSWORD=dummypassword
- MYSQL_DATABASE=social_media_database
- MYSQL_USER=social_media_user
- MYSQL_PASSWORD=dummypassword
healthcheck:
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
timeout: 20s
retries: 10
volumes:
- "./resources/db/init.sql:/docker-entrypoint-initdb.d/1.sql"
32 changes: 32 additions & 0 deletions rest-social-media/ballerina-social-media.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
### Creat a user
POST http://localhost:9090/social-media/users
content-type: application/json

{
"birthDate": {
"year": 1987,
"month": 02,
"day": 06
},
"name": "Rimas"
}

### Get users
GET http://localhost:9090/social-media/users

### Get a specific user
GET http://localhost:9090/social-media/users/1

### Get posts
GET http://localhost:9090/social-media/users/3/posts

### Create a post
POST http://localhost:9090/social-media/users/3/posts
content-type: application/json

{
"description": "I wang to learn GCP"
}

### Delete a user
DELETE http://localhost:9090/social-media/users/1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
DELETE http://localhost:9090/social-media/users/1
DELETE http://localhost:9090/social-media/users/1

8 changes: 8 additions & 0 deletions rest-social-media/ballerina/Ballerina.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
org = "integration_samples"
name = "ballerina_social_media"
version = "0.0.1"
distribution = "2201.7.0"

[build-options]
observabilityIncluded = true
8 changes: 8 additions & 0 deletions rest-social-media/ballerina/Cloud.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[container.image]
repository="integrationsamples"
name="ballerina-social-media"
tag="0.0.1"

[[container.copy.files]]
sourceFile="./Config.toml"
target="./Config.toml"
8 changes: 8 additions & 0 deletions rest-social-media/ballerina/Config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
moderate = true

[databaseConfig]
host = "localhost"
port = 3306
user = "social_media_user"
password = "dummypassword"
database = "social_media_database"
49 changes: 49 additions & 0 deletions rest-social-media/ballerina/response_error_interceptor.bal
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) 2023, WSO2 LLC. (http://www.wso2.org) All Rights Reserved.
//
// WSO2 LLC. licenses this file to you under the Apache License,
// Version 2.0 (the "License"); you may not use this file except
// in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

import ballerina/http;

// Handle listener errors
service class ResponseErrorInterceptor {
*http:ResponseErrorInterceptor;

remote function interceptResponseError(http:Request req, error err)
returns SocialMediaBadReqeust|SocialMediaServerError {
ErrorDetails errorDetails = buildErrorPayload(err.message(), req.rawPath);

if err is http:PayloadValidationError {
SocialMediaBadReqeust socialMediaBadRequest = {
body: errorDetails
};
return socialMediaBadRequest;
} else {
SocialMediaServerError socialMediaServerError = {
body: errorDetails
};
return socialMediaServerError;
}
}
}

type SocialMediaBadReqeust record {|
*http:BadRequest;
ErrorDetails body;
|};

type SocialMediaServerError record {|
*http:InternalServerError;
ErrorDetails body;
|};
26 changes: 26 additions & 0 deletions rest-social-media/ballerina/sentiment.bal
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (c) 2023, WSO2 LLC. (http://www.wso2.org) All Rights Reserved.
//
// WSO2 LLC. licenses this file to you under the Apache License,
// Version 2.0 (the "License"); you may not use this file except
// in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

type Probability record {
decimal neg;
decimal neutral;
decimal pos;
};

type Sentiment record {
Probability probability;
string label;
};
164 changes: 164 additions & 0 deletions rest-social-media/ballerina/social_media_service.bal
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// Copyright (c) 2023, WSO2 LLC. (http://www.wso2.org) All Rights Reserved.
//
// WSO2 LLC. licenses this file to you under the Apache License,
// Version 2.0 (the "License"); you may not use this file except
// in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

import ballerina/http;
import ballerina/sql;
import ballerina/mime;
import ballerinax/mysql.driver as _;
import ballerinax/mysql;
import ballerina/log;
import ballerina/time;

configurable boolean moderate = ?;

type DataBaseConfig record {|
string host;
int port;
string user;
string password;
string database;
|};
configurable DataBaseConfig databaseConfig = ?;

listener http:Listener socialMediaListener = new (9090);

service SocialMedia /social\-media on socialMediaListener {

final mysql:Client socialMediaDb;
final http:Client sentimentEndpoint;

public function init() returns error? {
self.socialMediaDb = check new (...databaseConfig);
self.sentimentEndpoint = check new("localhost:9099",
retryConfig = {
interval: 3
}
);
log:printInfo("Social media service started");
ayeshLK marked this conversation as resolved.
Show resolved Hide resolved
}

// Service-level error interceptors can handle errors occurred during the service execution.
public function createInterceptors() returns ResponseErrorInterceptor {
return new ResponseErrorInterceptor();
}

# Get all the users
#
# + return - The list of users or error message
resource function get users() returns User[]|error {
stream<User, sql:Error?> userStream = self.socialMediaDb->query(`SELECT * FROM social_media_database.user`);
return from User user in userStream
select user;
}

# Get a specific user
#
# + id - The user ID of the user to be retrived
# + return - A specific user or error message
resource function get users/[int id]() returns User|UserNotFound|error {
User|error result = self.socialMediaDb->queryRow(`SELECT * FROM social_media_database.user WHERE ID = ${id}`);
if result is sql:NoRowsError {
ErrorDetails errorDetails = buildErrorPayload(string `id: ${id}`, string `users/${id}/posts`);
UserNotFound userNotFound = {
body: errorDetails
};
return userNotFound;
}
return result;
}

# Create a new user
#
# + newUser - The user details of the new user
# + return - The created message or error message
resource function post users(NewUser newUser) returns http:Created|error {
_ = check self.socialMediaDb->execute(`
INSERT INTO social_media_database.user(birth_date, name)
VALUES (${newUser.birthDate}, ${newUser.name});`);
return http:CREATED;
}

# Delete a user
#
# + id - The user ID of the user to be deleted
# + return - The success message or error message
resource function delete users/[int id]() returns http:NoContent|error {
_ = check self.socialMediaDb->execute(`
DELETE FROM social_media_database.user WHERE id = ${id};`);
return http:NO_CONTENT;
}

# Get posts for a give user
#
# + id - The user ID for which posts are retrieved
# + return - A list of posts or error message
resource function get users/[int id]/posts() returns Post[]|UserNotFound|error {
User|error result = self.socialMediaDb->queryRow(`SELECT * FROM social_media_database.user WHERE id = ${id}`);
if result is sql:NoRowsError {
ErrorDetails errorDetails = buildErrorPayload(string `id: ${id}`, string `users/${id}/posts`);
UserNotFound userNotFound = {
body: errorDetails
};
return userNotFound;
}

stream<Post, sql:Error?> postStream = self.socialMediaDb->query(`SELECT id, description FROM social_media_database.post WHERE user_id = ${id}`);
Post[]|error posts = from Post post in postStream
select post;
return posts;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it okay if we just return the query expression here?

}

# Create a post for a given user
#
# + id - The user ID for which the post is created
# + return - The created message or error message
resource function post users/[int id]/posts(NewPost newPost) returns http:Created|UserNotFound|PostForbidden|error {
User|error result = self.socialMediaDb->queryRow(`SELECT * FROM social_media_database.user WHERE id = ${id}`);
if result is sql:NoRowsError {
ErrorDetails errorDetails = buildErrorPayload(string `id: ${id}`, string `users/${id}/posts`);
UserNotFound userNotFound = {
body: errorDetails
};
return userNotFound;
}

Sentiment sentiment = check self.sentimentEndpoint->/text\-processing/api/sentiment.post(
{ text: newPost },
mediatype = mime:APPLICATION_FORM_URLENCODED
);
if sentiment.label == "neg" {
ErrorDetails errorDetails = buildErrorPayload(string `id: ${id}`, string `users/${id}/posts`);
PostForbidden postForbidden = {
body: errorDetails
};
return postForbidden;
}

_ = check self.socialMediaDb->execute(`
INSERT INTO social_media_database.post(description, user_id)
VALUES (${newPost.description}, ${id});`);
return http:CREATED;
}
}

function buildErrorPayload(string msg, string path) returns ErrorDetails {
ErrorDetails errorDetails = {
message: msg,
timeStamp: time:utcNow(),
details: string `uri=${path}`
};
return errorDetails;
}
Loading