Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
david_kunz2
Advisor
Advisor

What is this blog series about?


 
A (private) hobby project where I'm building a simple server in the programming language Rust, borrowing some concepts of the SAP Cloud Application Programming Model (CAP).

 

But Why?




I love learning programming languages, especially the ones bringing novel ideas to the table since the past has repeatedly shown that radical software revolutions can happen.

 


Just to give three examples which changed your life:

  • Dijkstra's letter "Go To Statement Considered Harmful" (1968) which led to consensus about basic control flow statements

  • the programming language Smalltalk (Alan Kay, 1972) introducing object-oriented programming

  • LISP (John McCarthy, 1958) as the first functional programming language



Thus, it would be imprudent to only stick with the present set of common knowledge, so let's see what's out there.

 

What is Rust?



 
Rust (Graydon Hoare, 2010) is a community-driven multi-paradigm compiled programming language with "zero cost abstractions" (no additional runtime overhead).




The performance characteristics are comparable to languages like C and C++.

 


And it feels very high-level - it comes with


  • memory safety

  • an advanced type system (with type inference)

  • functional features (like closures, algebraic data types and pattern matching)

  • support for asynchronous programming (async/await)

  • generics and a trait system

  • a module system

  • a macro system

  • productivity tools (for example a very helpful compiler, package manager, code formatter and documentation generator, all bundled in the tool `cargo`)


and much more.



What's so special about it is that it guarantees memory safety without a garbage collector, accomplished by the concept of ownership, leading to low memory footprints and predictable performance.

Please have a look at the official website for more information.



According to Stack Overflow's developer survey, it's the most loved programming language for the fifth consecutive year.

 

Why CAP?


 
When learning new techniques it's best to apply them to well-known situations first.



Since I'm currently working on CAP, I'll build a server borrowing some of its concepts.

 


Our server will

  • read a compiled CDS model (in JSON representation)

  • register simple generic CREATE and READ handlers

  • use PostgreSQL as a database

  • be super fast



A Guided Tour


 
Let's begin our journey with a guided tour of some of the source code of our Rust-CAP server.

It's impossible to explain all nitty-gritty details in a few blog posts, so I'll try to keep it short and crisp

and focus on the highlights.



For the sake of clarity I'll depict unimportant things with `...`.

The working source code can be found on GitHub.

Server


 

Let's start with a simple web server.

There are numerous libraries/frameworks out there, this blog post gives you a good overview. I chose `tide` because it's quite slim and I'm not using a lot of features anyway.

All Rust programs must have a `main` function which can return something called an `io::Result<()>`.
fn main() -> io::Result<()> { ... }

 

Error Handling


 
Rust has excellent documentation, so let's see what this type is.

It boils down to


enum Result<T, E> {
Ok(T),
Err(E),
}


which is very similar to error handling in functional languages like Haskell or OCaml.

It's an enum, meaning it can either be `Ok(T)` or `Err(E)` with generic types `T` and `E`.

If a call to a function returns `Ok(T)` we know that everything went fine and can continue

with `T`.



This is usually done with pattern matching:


let number_str = "10";
let number = match number_str.parse::<i32>() { // try to parse the number to an i32 (this will return a Result)
Ok(number) => number, // if it works, extract the number
Err(e) => return Err(e), // if not, return Err(e)
};



There's also syntactic sugar for this:


let number_str = "10";
let number = number_str.parse::<i32>()?;




Notice the `?` operator.




Now let's head back to our server.


fn main() -> io::Result<()> {
task::block_on(async { // spawns a task and blocks the current thread on its result, now we can use `await`
let state = State { ... }; // we can use this state object in our request handlers
let mut app = Server::with_state(state); // start a server with state
add_routes::add_routes(&mut app, ...); // add some routes
let url = "127.0.0.1:8080";
println!("Server listening on http://{}", &url);
app.listen(&url).await?;
Ok(())
})
}

Rust essentially has no runtime. In contrast to Node.js, to write an async program one needs an executor from a library, this is where `task` comes from.

 
Let's add some routes


fn add_routes(app: &mut Server<State>, ...) -> () {
app.at("/").get(|_req: tide::Request<State>| async move {
Ok(
tide::Response::new(StatusCode::Ok)
.body_string("Please use proper routes.".to_string()),
)
});
}

In Rust, closures can be created with the following syntax.



|...| { ... }



To start our server we can simply use the command-line tool `cargo`.

$ cargo run

GET http://127.0.0.1:8080/

Please use proper routes.


Now we've got a running server.

Let's concentrate on the main building blocks of CAP.

Core Query Notation


One of the most important things in CAP is our core query notation, or `CQN` in short.

It let's you define queries in an abstract way to be run against databases or REST/OData services.

Let's see how that can be done in Rust.


#[derive(Debug)]
enum CQN {
SELECT(SELECT),
INSERT(INSERT),
}

Our `CQN` is either a `SELECT` or an `INSERT`, we'll ignore `UPDATE` and `DELETE` for now.

The strange `#[derive(Debug)]` line is a macro. You can think of it as a way to automatically

derive additional Rust code, minimizing the amount of boilerplate code you need to write.



This particular macro lets you print this enum to the console whenever you want.


Let's see how `SELECT` is defined.

Structs


#[derive(Debug)]
struct SELECT {
entity: Identifier,
columns: Vec<Identifier>,
filter: Vec<String>,
}

A struct is basically a group of fields with arbitrary types.

Here, we have something that defines an entity, the wanted columns and a filter.

`Vec<String>` just means a vector of strings, think of it as an array of variable size.

Our `Identifier` is constructed to identify entities or columns.
#[derive(Debug)]
struct Identifier {
reference: Vec<String>,
alias: Option<String>,
}

The `Option` enum is very similar to our `Result` enum. We use it to say that this
field is optional.

Having a look into the Rust documentation yields:
enum Option<T> {
None,
Some(T),
}

 
So an Option of type `T` is either `None` or `Some(T)`. Again, exactly how functional languages do it.


What's great about structs is that you can define methods on them.


impl SELECT {
fn from(entity: &str) -> SELECT {
SELECT {
entity: Identifier { reference: vec![entity.to_string()], alias: None },
columns: vec![],
filter: vec![],
}
}
...
}



Think of this as a static method `from` which produces a `SELECT` itself, kind of like a named constructor.

You can call it like this:

let select = SELECT::from("example_entity");


It's also possible to define methods on instances, the only thing you need to do is to change the arguments to include some form of `self`. Here, it's a mutable reference `&mut self`, that means you're allowed to mutate itself.


impl SELECT {
...
fn columns(&mut self, columns: Vec<&str>) -> &mut Self {
let cols: Vec<Identifier> = columns
.iter()
.map(|col| Identifier {
reference: vec![col.to_string()],
alias: None
})
.collect();
self.columns.extend(cols);
self // implicit return
}
...
}



We use `map` to change a vector of strings to a vector of `Identifier`s. As simple as that.

 


Polymorphism


In Rust, polymorphism is achieved through generics and traits. You already saw generics in our enums, it also works in functions and structs.

A trait is similar to an interface, it's something a struct can implement. Take this example:


trait SQL {
fn to_sql(&self) -> String;
}




Here, we define the `SQL` trait. If you want to implement it for a particular struct, you have to implement the `to_sql` function with the same signature. It's also possible to provide default implementations, similar to abstract classes.

 


Let's implement `SQL` for our `SELECT` struct.

impl SQL for SELECT {
fn to_sql(&self) -> String {
let from_sql = &self.entity.reference.join(".").to_string().replace(".", "_");
let mut res = match &self.columns.len() > &0 {
true => {
let cols: Vec<String> = self
.columns
.iter()
.map(|c| c.reference.join(".").to_string())
.collect();
format!("SELECT {} FROM {}", cols.join(","), &from_sql)
}
false => format!("SELECT * FROM {}", &from_sql),
};
if &self.filter.len() > &0 {
res = format!("{}{}", res, format!(" WHERE {}", &self.filter.join(" ")));
}
res
}
}


Now, every `SELECT` struct can be converted to an SQL string.

If you're writing a function that just needs something which implements the `SQL` trait, you can simply write the following.


fn my_function(something: &impl SQL) {
println!("My SQL string is {}", something.to_sql());
}


Automatic Testing


Rust comes with a testing framework built-in, you only need to use a few macros.


#[cfg(test)] // this is our testing section
mod tests {
use super::*;
#[test] // define a test
fn select_with_col_to_sql() {
let mut select = SELECT::from("example_entity");
select.columns(vec!["col1", "col2"]);
assert_eq!(select.to_sql(), "SELECT col1,col2 FROM example_entity")
}
}



Run `cargo test` and see if it works.


$ cargo test
test cqn::tests::select_with_col_to_sql ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out


Conclusion


We've seen some basic features of the Rust programming language and how easy it is so create a running server and implement some of CAP's elements. I hope I could convince you that Rust does not feel as low level as one would expect given its high performance and that it comes with useful features and tools.

Next Steps


In the next blog post we'll integrate more CAP concepts into the world of Rust. Stay tuned!
4 Comments