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 25 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5d267ab
Add initial structure for ballerina rest-social-media service
ayeshLK Jul 10, 2023
8a2b674
Add spring-boot social-media service
ayeshLK Jul 10, 2023
b1e5629
Disable tests for time-being
ayeshLK Jul 11, 2023
eda8dd0
Add spring-boot-reactive social-media service
ayeshLK Jul 11, 2023
6f4cbaf
Update the social-media service configurations
ayeshLK Jul 11, 2023
e2fc083
Add db-init scripts
ayeshLK Jul 11, 2023
462c9b6
Add the readme file
ayeshLK Jul 11, 2023
4861e60
Add relevant docker compose files
ayeshLK Jul 11, 2023
14788d2
Add relevant .http files for VSCode REST client
ayeshLK Jul 11, 2023
d52aff6
Refactor the MySQL init script
ayeshLK Jul 11, 2023
05dbbdd
Refactor the DB entity
ayeshLK Jul 11, 2023
4e2d9c8
Add missing image
ayeshLK Jul 11, 2023
066942e
Add missing config annotation
ayeshLK Jul 11, 2023
090c45c
Update docker compose configurations
ayeshLK Jul 11, 2023
b3352a4
Add retry config
ayeshLK Jul 11, 2023
81b3010
Fix configurations
ayeshLK Jul 11, 2023
3fdaba9
Fix controller method mapping
ayeshLK Jul 11, 2023
745a0ae
Update VSCode REST client configs
ayeshLK Jul 11, 2023
2f758f7
Update SQL queries
ayeshLK Jul 11, 2023
1564c07
Refactor SQL queries
ayeshLK Jul 11, 2023
025a652
Merge remote-tracking branch 'upstream/main' into springboot-v-ballerina
ayeshLK Jul 27, 2023
8711761
Incorporate review suggestions
ayeshLK Jul 27, 2023
b9b3316
Refactor response-error interceptor
ayeshLK Jul 27, 2023
d5a67e3
Update social-media service type
ayeshLK Jul 27, 2023
d98127a
Update rest-social-media/ballerina-social-media.http
anupama-pathirage Nov 26, 2024
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
anupama-pathirage marked this conversation as resolved.
Show resolved Hide resolved
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"
50 changes: 50 additions & 0 deletions rest-social-media/ballerina/response_error_interceptor.bal
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// 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/constraint;

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

remote function interceptResponseError(http:RequestContext ctx, error err)
ayeshLK marked this conversation as resolved.
Show resolved Hide resolved
returns SocialMediaBadReqeust|SocialMediaServerError {
ErrorDetails errorDetails = buildErrorPayload(err.message(), "");
ayeshLK marked this conversation as resolved.
Show resolved Hide resolved

if err is constraint:Error {
ayeshLK marked this conversation as resolved.
Show resolved Hide resolved
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;
};
162 changes: 162 additions & 0 deletions rest-social-media/ballerina/social_media_service.bal
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// 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,
interceptors = [new ResponseErrorInterceptor()]
ayeshLK marked this conversation as resolved.
Show resolved Hide resolved
);

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
}

# 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;
} else {
ayeshLK marked this conversation as resolved.
Show resolved Hide resolved
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(@http:Payload NewUser newUser) returns http:Created|error {
ayeshLK marked this conversation as resolved.
Show resolved Hide resolved
_ = 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(@http:Payload 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