Initial project import
This commit is contained in:
commit
23f2d29e32
11957 files changed
+1754577
No files matched your search
@@ -0,0 +1,337 @@
|
||||
In this tutorial I'll demonstrate C2V, a fully automatic C to V source code translator,
|
||||
that has just been released.
|
||||
|
||||
We'll run some simple examples and then translate the entire original DOOM,
|
||||
the legendary shooter, the first ever 3D game released in 1993 and open sourced in 1997.
|
||||
|
||||
A video version of this tutorial is available on YouTube:
|
||||
|
||||
https://www.youtube.com/watch?v=6oXrz3oRoEg
|
||||
|
||||
Let's start.
|
||||
|
||||
### A simple C program
|
||||
|
||||
First, We have a simple C program that prints prime numbers. It's a small program,
|
||||
but it has some of the most widely used language features:
|
||||
function definitions, control flow via for loops and if conditions, a libc function call,
|
||||
and various expressions.
|
||||
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
bool is_prime(int x) {
|
||||
for (int i = 2; i <= x / 2; i++) {
|
||||
if (x % i == 0) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int main() {
|
||||
for (int i = 2; i < 100; i++) {
|
||||
if (is_prime(i)) {
|
||||
printf("%i\n", i);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
We can translate the C file to V by simply running
|
||||
|
||||
```
|
||||
v translate primes.c
|
||||
```
|
||||
|
||||
It will create `primes.v` with the following contents:
|
||||
|
||||
|
||||
```v
|
||||
@[translated]
|
||||
module main
|
||||
|
||||
fn is_prime(x int) bool {
|
||||
for i := 2; i <= x / 2; i++ {
|
||||
if x % i == 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fn main() {
|
||||
for i := 2; i < 100; i++ {
|
||||
if is_prime(i) {
|
||||
C.printf(c'%i\n', i)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
This is a valid V program, and running it will give us the same result.
|
||||
|
||||
This code is pretty similar to the original, you can notice one major difference.
|
||||
C allows one statement code blocks without brackets, and V doesn't, so in the
|
||||
translated code all blocks are explicitly marked with brackets.
|
||||
|
||||
C2V successfully converts C's bool type to V's bool.
|
||||
|
||||
### Wrapper generation
|
||||
|
||||
C2V also has a wrapper generation mode.
|
||||
|
||||
For many projects it would make more sense not to translate the entire C code base,
|
||||
but just to generate V wrappers on top of C APIs.
|
||||
|
||||
For example, let's say you have some complicated crypto or networking library in C
|
||||
with lots of functions and types, and you don't want to manually type their definitions in V.
|
||||
|
||||
For this C2V has a wrapper mode.
|
||||
|
||||
We have a simple file `usersapi.h`. By running `v translate wrapper usersapi.c`
|
||||
we get `usersapi.v` with just the function definitions. Function bodies are skipped.
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
|
||||
void usersapi_create_user(char* name, char* password, int age) {
|
||||
// ...
|
||||
puts("User created!");
|
||||
}
|
||||
|
||||
int usersapi_get_number_of_users() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
```v oksyntax
|
||||
module usersapi
|
||||
|
||||
fn C.usersapi_create_user(name &i8, password &i8, age int)
|
||||
|
||||
pub fn create_user(name &i8, password &i8, age int) {
|
||||
C.usersapi_create_user(name, password, age)
|
||||
}
|
||||
|
||||
fn C.usersapi_get_number_of_users() int
|
||||
|
||||
pub fn get_number_of_users() int {
|
||||
return C.usersapi_get_number_of_users()
|
||||
}
|
||||
```
|
||||
|
||||
C doesn't have modules, so in most C libraries all functions start with the same prefix,
|
||||
the name of the virtual module. C2V helpfully strips those prefixes and generates nice
|
||||
clean wrapper functions that can be called with `usersapi.create_user()` instead of
|
||||
`usersapi.usersapi_create_user()`
|
||||
|
||||
Perhaps in the future C2V will translate C strings (char pointers) to V strings as well.
|
||||
|
||||
### Translating DOOM
|
||||
|
||||
All right, this works, and it's nice, but these are very trivial examples.
|
||||
Let's try something a lot more fun, like the DOOM game.
|
||||
|
||||
|
||||
We think it's very important to have large functioning examples to demonstrate the power
|
||||
of the tools but also to have something to test via CI to avoid regressions.
|
||||
During V's release we had lots of various examples, now for C2V we have DOOM.
|
||||
This is the primary test of C2V.
|
||||
|
||||
DOOM is a very complex project that uses some really tricky elements of the C language.
|
||||
And making sure C2V can always handle DOOM not only serves as a good C2V test but also
|
||||
as a test of the V compiler itself, since it can run entire DOOM in pure V.
|
||||
|
||||
First let's download the DOOM source and build it to make sure everything works.
|
||||
The original 1997 open source release doesn't work nicely on modern OSs, so the community
|
||||
has been working on different forks. I'll be using Chocolate Doom, it's one of the most
|
||||
popular forks, and its goal is to be as close to the original as possible
|
||||
(they even keep the original bugs).
|
||||
|
||||
|
||||
```bash
|
||||
git clone https://github.com/vlang/doom
|
||||
```
|
||||
|
||||
The main difference in this fork is using SDL for cross platform rendering, so we'll need
|
||||
to install SDL2 before we can build it.
|
||||
|
||||
A great thing about DOOM is that it has a very simple structure and is easy to build.
|
||||
It's just a bunch of C files that are compiled into object files and are later linked
|
||||
into a single executable.
|
||||
|
||||
To build the C version:
|
||||
|
||||
```bash
|
||||
cd doom/chocolate-doom
|
||||
cmake .
|
||||
make chocolate-doom
|
||||
```
|
||||
|
||||
We'll also need a freely distributed shareware doom1.wad with game resources and
|
||||
the first free levels.
|
||||
|
||||
Now let's run the compiled game
|
||||
|
||||
```bash
|
||||
./src/chocolate-doom -width 640
|
||||
```
|
||||
|
||||
Great, as expected, it all works, now let's translate it to V and try to build that.
|
||||
|
||||
|
||||
In the previous example we translated a single file, this time we'll translate
|
||||
an entire directory.
|
||||
|
||||
```
|
||||
v translate src/doom
|
||||
```
|
||||
|
||||
This creates a new directory `src/doom/doom_v/`.
|
||||
|
||||
And we can use any name we want, there's a c2v configuration file.
|
||||
|
||||
`ll src/doom/doom_v`
|
||||
|
||||
It has a bunch of files translated from C to V.
|
||||
|
||||
We can open any file, for example g_game.c and verify that it is indeed DOOM code
|
||||
translated from C to V. It's also been nicely formatted by vfmt.
|
||||
|
||||
Now let's build it.
|
||||
|
||||
|
||||
```bash
|
||||
cd src/doom
|
||||
sh build_doom_v.sh
|
||||
```
|
||||
|
||||
I'll explain what we have in the shell script a bit later.
|
||||
|
||||
But first let's run the translated code!
|
||||
|
||||
The script compiles into the `src/doom/doomv` binary. Everything works. We have the music,
|
||||
we can watch the demo, go to the settings, start the game, walk around and shoot enemies.
|
||||
|
||||
There's a slight rendering bug at the bottom, but it can be cleaned up by pressing tab twice
|
||||
and forcing a re-render.
|
||||
|
||||
There's a bug somewhere in C2V, it has just been released and needs some fixes.
|
||||
|
||||
|
||||
Now let's look at those shell scripts and see how we built the V translation of DOOM.
|
||||
|
||||
C2V doesn't understand makefiles yet, so we have to do things manually.
|
||||
|
||||
In the future making such scripts won't be necessary.
|
||||
|
||||
|
||||
Since doom uses C libraries like textscreen, pcsound, and opl, and we are only translating
|
||||
doom itself, we need to build doom.o and then link it with the rest of object files.
|
||||
|
||||
`build_doom_v.sh`:
|
||||
|
||||
```bash
|
||||
export DOOM=~/code/doom/chocolate-doom
|
||||
|
||||
v -o doom_v/doom.o -w -translated doom_v
|
||||
|
||||
|
||||
cc -o doomv \
|
||||
$DOOM/src/doom/doom_v/doom.o \
|
||||
$DOOM/src/CMakeFiles/chocolate-doom.dir/*.o \
|
||||
$DOOM/textscreen/CMakeFiles/textscreen.dir/*.o \
|
||||
$DOOM/pcsound/CMakeFiles/pcsound.dir/*.o \
|
||||
$DOOM/opl/CMakeFiles/opl.dir/*.o \
|
||||
$(sdl2-config --libs) -lSDL2_mixer -lSDL2_net -lpng $LDFLAGS
|
||||
```
|
||||
|
||||
The first line compiles all V files in `doom_v/` directory into a single doom.o object file.
|
||||
`-translated` tells V that the code was translated from C, so the compiler is less strict.
|
||||
For example it allows using `i++` as an expression (in V it always must be a statement).
|
||||
In the future C2V will be able to translate such C expressions into valid V code,
|
||||
and `-translated` will be removed entirely.
|
||||
|
||||
After generating doom.o, we can compile the entire project.
|
||||
|
||||
|
||||
The second line links it with the rest of the object files.
|
||||
|
||||
Here we have the shared library used by other games in the chocolate doom project:
|
||||
Hexen, Heretic, and Strife.
|
||||
|
||||
Then textscreen, pcsound, and opl.
|
||||
|
||||
|
||||
In the near future we'll also translate all these C libraries DOOM depends on, and this
|
||||
shell script and interacting with the C compiler won't be needed. The entire project will
|
||||
be runnable with a simple `v translate src/doom && v .`
|
||||
|
||||
|
||||
|
||||
One other thing I wanted to do is to show you that it actually is V code we are running.
|
||||
|
||||
Let's go to `p_enemy.v` and modify the `P_Move` function. We can add a simple println with
|
||||
some V specific code, like a reversed V array.
|
||||
|
||||
|
||||
`println([1, 2, 3, 4, 5].reverse())`
|
||||
|
||||
Let's rebuild it and run again. As expected, we're getting our array printed every time an
|
||||
enemy moves.
|
||||
|
||||
### Replacing C with V file by file
|
||||
|
||||
We translated the entire project. But sometimes it may be better to do more gradual transition
|
||||
from C to V. For example, if you want to move your project from C to V, you might prefer doing
|
||||
it slowly and carefully, file by file. After each file migration you can run tests and see if
|
||||
your application runs correctly.
|
||||
|
||||
You can do that with c2v. All you need to do is just translate a single file, generate an object
|
||||
file, and replace your current C object file with the new V object file.
|
||||
|
||||
We can do this with DOOM too. In fact that's how we made the entire thing work. We were doing it
|
||||
for every single file, one by one, fixing compilation errors, bugs in C2V, and runtime bugs.
|
||||
|
||||
```
|
||||
c2v -o p_enemy.o p_enemy.v
|
||||
cp p_enemy.o ...
|
||||
make clean
|
||||
make chocolate-doom
|
||||
```
|
||||
|
||||
|
||||
It is actually amazing how well it all works considering there have been two translation
|
||||
processes. From C to V via C2V and then From V back to C again via the V compiler.
|
||||
|
||||
In fact we can actually look at the generated C code after all the translation and compare it
|
||||
to the original. Very clean and barely changed:
|
||||
|
||||

|
||||
|
||||
Both C2V and V generate clean human readable code.
|
||||
|
||||
|
||||
The comments and C defines are missing right now, that's obviously something very nice to have,
|
||||
so they will be translated as well.
|
||||
|
||||
|
||||
### Conclusion
|
||||
|
||||
|
||||
C2V has just been released on June 22th 2022, so it's going to mature with the help of
|
||||
the community. With bug fixes and new C features supported, it should support the
|
||||
entire C standard soon.
|
||||
Luckily C is a relatively small language. Any kind of software written in C will be
|
||||
translatable to V fully automatically.
|
||||
|
||||
I will also be publishing the same demo with Sqlite and Quake translation.
|
||||
|
||||
It's a huge milestone for V and gives V developers access to huge amounts of software
|
||||
written in C.
|
||||
|
||||
We're very excited about this release.
|
||||
@@ -0,0 +1,432 @@
|
||||
# Building a 400 KB Web Blog in V & SQLite with Veb
|
||||
|
||||
Hello,
|
||||
|
||||
In this guide, we'll build a simple web blog using V and the Veb framework.
|
||||
|
||||
The benefits of using V and Veb for web development:
|
||||
|
||||
- A safe, fast language with the development agility of Python or Ruby and the performance of C
|
||||
- Zero dependencies: everything needed comes in a 1 MB package
|
||||
- Very small binaries: this blog will be about 150 KB
|
||||
- Easy deployments: single binary including precompiled templates
|
||||
- Runs on minimal hardware: $3 instance sufficient for most apps
|
||||
- Fast development with minimal boilerplate
|
||||
|
||||
> [!NOTE]
|
||||
> V and Veb are at an early stage and evolving rapidly.
|
||||
|
||||
The code is available [here](./code/blog).
|
||||
|
||||
## Installing V
|
||||
```
|
||||
wget --quiet https://github.com/vlang/v/releases/latest/download/v_linux.zip
|
||||
unzip v_linux.zip
|
||||
cd v
|
||||
sudo ./v symlink
|
||||
```
|
||||
|
||||
|
||||
V should now be globally available.
|
||||
|
||||
> On macOS use `v_macos.zip`, on Windows - `v_windows.zip`.
|
||||
> For BSD, Solaris, Android, or source installation, see:
|
||||
> https://github.com/vlang/v#installing-v-from-source
|
||||
|
||||
## Install SQLite Development Dependency
|
||||
|
||||
See [`sqlite` README](../../vlib/db/sqlite/README.md) for instructions if not already installed.
|
||||
|
||||
## Creating a New Veb Project
|
||||
|
||||
V projects can be created anywhere:
|
||||
|
||||
```bash
|
||||
mkdir blog
|
||||
cd blog
|
||||
touch blog.v
|
||||
```
|
||||
|
||||
First, let's create a simple hello world website:
|
||||
|
||||
```v oksyntax
|
||||
// blog.v
|
||||
module main
|
||||
|
||||
import veb
|
||||
|
||||
pub struct Context {
|
||||
veb.Context
|
||||
}
|
||||
|
||||
pub struct App {}
|
||||
|
||||
fn main() {
|
||||
mut app := &App{}
|
||||
veb.run[App, Context](mut app, 8081)
|
||||
}
|
||||
|
||||
pub fn (app &App) index(mut ctx Context) veb.Result {
|
||||
return ctx.text('Hello world from Veb!')
|
||||
}
|
||||
```
|
||||
|
||||
Run it with
|
||||
|
||||
```bash
|
||||
v run blog.v
|
||||
```
|
||||
|
||||
```
|
||||
Running a Veb app on http://localhost:8081 ...
|
||||
```
|
||||
|
||||
Veb helpfully provided a link, open http://localhost:8081/ in your browser:
|
||||
|
||||
<img width=662 src="https://github.com/vlang/v/blob/master/tutorials/building_a_simple_web_blog_with_veb/img/hello.png?raw=true">
|
||||
|
||||
The App struct holds shared application data, while Context handles per-request data and embeds
|
||||
`veb.Context` for response methods like `.text()`.
|
||||
|
||||
If you have experiencewith an MVC web framework, you can think of it as a controller. (Veb is
|
||||
not an MVC framework however.)
|
||||
|
||||
|
||||
|
||||
As you can see, there are no routing rules. The `index()` action handles the `/` request by default.
|
||||
Veb often uses convention over configuration and adding a new action requires
|
||||
no routing rules either:
|
||||
|
||||
```v oksyntax
|
||||
// blog.v
|
||||
import veb
|
||||
import time
|
||||
|
||||
fn (app &App) time(mut ctx Context) veb.Result {
|
||||
return ctx.text(time.now().format())
|
||||
}
|
||||
```
|
||||
|
||||
Custom routes can be defined using attributes like @['/index'].
|
||||
|
||||
<img width=662 src="https://github.com/vlang/v/blob/master/tutorials/building_a_simple_web_blog_with_veb/img/time.png?raw=true">
|
||||
|
||||
> TIP: run the following command to live-reload the server: `v watch run blog.v`
|
||||
|
||||
The `.text(string)` method returns a plain text document with the provided
|
||||
text, which isn't frequently used in websites.
|
||||
|
||||
### HTML View
|
||||
|
||||
Let's return an HTML view instead. Create `index.html` in the same directory:
|
||||
|
||||
```html
|
||||
<html>
|
||||
<head>
|
||||
<title>V Blog</title>
|
||||
</head>
|
||||
<body>
|
||||
<b>@message</b>
|
||||
<br />
|
||||
<img src="https://vlang.io/img/v-logo.png" width="100" />
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
and update our `index()` action so that it returns the HTML view we just created:
|
||||
|
||||
```v ignore
|
||||
// blog.v
|
||||
pub fn (app &App) index(mut ctx Context) veb.Result {
|
||||
message := 'Hello, world from Veb!'
|
||||
return $veb.html()
|
||||
}
|
||||
```
|
||||
|
||||
<img width=662 src="https://github.com/vlang/v/blob/master/tutorials/building_a_simple_web_blog_with_veb/img/hello_html.png?raw=true">
|
||||
|
||||
Good, now we have an actual HTML page.
|
||||
|
||||
The V template language is similar to C#'s Razor: `@message` prints the value
|
||||
of `message`.
|
||||
|
||||
You may notice something unusual: the `message` variable created in the `index()`
|
||||
action is automatically available in the view.
|
||||
|
||||
It's another feature of Veb to reduce the boilerplate in your web apps.
|
||||
No need to create view models just to pass data, or use an unsafe and untyped
|
||||
alternative, like C#'s `ViewBag["message"]`.
|
||||
|
||||
Making all action variables available in the view may seem crazy,
|
||||
but V is a language with pure functions by default, and you won't be able
|
||||
to modify any data from a view. `<b>@foo.bar()</b>` will only work if the `bar()` method
|
||||
doesn't modify `foo`.
|
||||
|
||||
The HTML template is compiled to V during the compilation of the website,
|
||||
that's done by the `$veb.html()` line.
|
||||
(`$` always means compile time actions in V.) offering the following benefits:
|
||||
|
||||
- Great performance, since the templates don't need to be compiled
|
||||
on every request, like in almost every major web framework.
|
||||
|
||||
- Easier deployment, since all your HTML templates are compiled
|
||||
into a single binary file together with the web application itself.
|
||||
|
||||
- All errors in the templates are guaranteed to be caught during compilation.
|
||||
|
||||
### Fetching data with V ORM
|
||||
|
||||
Now let's display some articles!
|
||||
|
||||
We'll be using [V's built-in ORM](https://docs.vlang.io/orm.html) and a SQLite database.
|
||||
|
||||
Add a SQLite handle to `App`:
|
||||
|
||||
```v oksyntax
|
||||
// blog.v
|
||||
import db.sqlite
|
||||
import veb
|
||||
|
||||
struct App {
|
||||
// ...
|
||||
db sqlite.DB
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
In `fn main()` we'll connect to a database.
|
||||
Code in the `main()` function is run only once during app's startup, so we are going
|
||||
to have one DB connection for all requests. This improves the performance of the web application,
|
||||
since a DB connection doesn't have to be set up for each request.
|
||||
|
||||
```v oksyntax
|
||||
import veb
|
||||
|
||||
// blog.v
|
||||
fn main() {
|
||||
mut app := &App{
|
||||
db: sqlite.connect(':memory:')!
|
||||
}
|
||||
sql app.db {
|
||||
create table Article
|
||||
}!
|
||||
|
||||
first_article := Article{
|
||||
title: 'Hello, world!'
|
||||
text: 'V is great.'
|
||||
}
|
||||
|
||||
second_article := Article{
|
||||
title: 'Second post.'
|
||||
text: 'Hm... what should I write about?'
|
||||
}
|
||||
|
||||
sql app.db {
|
||||
insert first_article into Article
|
||||
insert second_article into Article
|
||||
}!
|
||||
veb.run[App, Context](mut app, 8081)
|
||||
}
|
||||
```
|
||||
|
||||
Create a new file `article.v`:
|
||||
|
||||
```v oksyntax
|
||||
// article.v
|
||||
module main
|
||||
|
||||
struct Article {
|
||||
id int @[primary; sql: serial]
|
||||
title string
|
||||
text string
|
||||
}
|
||||
|
||||
pub fn (app &App) find_all_articles() []Article {
|
||||
return sql app.db {
|
||||
select from Article
|
||||
} or { panic(err) }
|
||||
}
|
||||
```
|
||||
|
||||
Notice that the `Article` structure conforms to the same structure and naming as
|
||||
the database table in the creation SQL statement. Also we need to add ORM decorators
|
||||
to our primary key to let it know that it is the primary key and it should auto-increment
|
||||
|
||||
Let's fetch the articles in the `index()` action:
|
||||
|
||||
```v ignore
|
||||
// blog.v
|
||||
pub fn (app &App) index() veb.Result {
|
||||
articles := app.find_all_articles()
|
||||
return $veb.html()
|
||||
}
|
||||
```
|
||||
|
||||
Finally, let's update our view:
|
||||
|
||||
```html
|
||||
<body>
|
||||
@for article in articles
|
||||
<div>
|
||||
<b>@article.title</b> <br />
|
||||
@article.text
|
||||
</div>
|
||||
@end
|
||||
</body>
|
||||
```
|
||||
|
||||
```bash
|
||||
v run .
|
||||
```
|
||||
|
||||
<img width=662 src="https://github.com/vlang/v/blob/master/tutorials/building_a_simple_web_blog_with_veb/img/articles1.png?raw=true">
|
||||
|
||||
That was very simple, wasn't it?
|
||||
|
||||
The built-in V ORM uses a syntax very similar to SQL. The queries are built with V.
|
||||
For example, if we only wanted to find articles with ids between 100 and 200, we'd do:
|
||||
|
||||
```v oksyntax
|
||||
// article.v
|
||||
|
||||
return sql app.db {
|
||||
select from Article where id >= 100 && id <= 200
|
||||
} or { panic(err) }
|
||||
```
|
||||
|
||||
Retrieving a single article is very simple:
|
||||
|
||||
```v oksyntax
|
||||
// article.v
|
||||
pub fn (app &App) retrieve_article() ?Article {
|
||||
return sql app.db {
|
||||
select from Article limit 1
|
||||
} or { panic(err) }[0]
|
||||
}
|
||||
```
|
||||
|
||||
V ORM uses V's optionals for single values, which is very useful, since
|
||||
bad queries will always be handled by the developer:
|
||||
|
||||
```v ignore
|
||||
// article.v
|
||||
article := app.retrieve_article() or {
|
||||
return ctx.text('Article not found')
|
||||
}
|
||||
```
|
||||
|
||||
### Adding new articles
|
||||
|
||||
Create `new.html`:
|
||||
|
||||
```html
|
||||
<html>
|
||||
<head>
|
||||
<title>V Blog</title>
|
||||
</head>
|
||||
<body>
|
||||
<form action="/new_article" method="post">
|
||||
<input type="text" placeholder="Title" name="title" /> <br />
|
||||
<textarea placeholder="Text" name="text"></textarea>
|
||||
<input type="submit" />
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
```v ignore
|
||||
@['/new']
|
||||
pub fn (app &App) new(mut ctx Context) veb.Result {
|
||||
return $veb.html()
|
||||
}
|
||||
|
||||
@['/new_article'; post]
|
||||
pub fn (app &App) new_article(mut ctx Context) veb.Result {
|
||||
title := ctx.form['title'] or { '' }
|
||||
text := ctx.form['text'] or { '' }
|
||||
|
||||
if title == '' || text == '' {
|
||||
return ctx.text('Empty text/title')
|
||||
}
|
||||
|
||||
article := Article{
|
||||
title: title
|
||||
text: text
|
||||
}
|
||||
sql app.db {
|
||||
insert article into Article
|
||||
} or { panic(err) }
|
||||
return ctx.redirect('/')
|
||||
}
|
||||
```
|
||||
|
||||
The decorator on our function tells Veb that it is an HTTP POST type operation.
|
||||
|
||||
This time Veb parses the HTTP form and assigns correct values with correct types to
|
||||
function arguments, which saves a lot of typing (e.g. `title := app.form['title']` is
|
||||
not necessary).
|
||||
|
||||
We need to update `index.html` to add a link to the "new article" page:
|
||||
|
||||
```html
|
||||
<a href="/new">New article</a>
|
||||
```
|
||||
|
||||
Next we need to add the HTML endpoint to our code like we did with `index.html`:
|
||||
|
||||
```v ignore
|
||||
@['/new']
|
||||
pub fn (app &App) new() veb.Result {
|
||||
return $veb.html()
|
||||
}
|
||||
```
|
||||
|
||||
Re-running this code will now allow us to add new posts to our blog endpoint
|
||||
|
||||
### JSON endpoints
|
||||
|
||||
This tutorial used the traditional server-side rendering. If you prefer
|
||||
to render everything on the client or need an API, creating JSON endpoints
|
||||
in V is very simple:
|
||||
|
||||
```v oksyntax
|
||||
import veb
|
||||
|
||||
@['/articles'; get]
|
||||
pub fn (app &App) articles(mut ctx Context) veb.Result {
|
||||
articles := app.find_all_articles()
|
||||
return ctx.json(articles)
|
||||
}
|
||||
```
|
||||
|
||||
<img width=662 src="https://github.com/vlang/v/blob/master/tutorials/building_a_simple_web_blog_with_veb/img/articles_json.png?raw=true">
|
||||
|
||||
### Persistent data
|
||||
|
||||
If one wants to persist data they need to use a file instead of memory SQLite Database.
|
||||
Replace the db setup code with this instead:
|
||||
|
||||
```
|
||||
db: sqlite.connect('blog.db')!
|
||||
```
|
||||
|
||||
As we can see it attempts to open a file in the current directory named `blog.db`.
|
||||
If the database file doesn't exist it will create it. The second command will
|
||||
create the table `Article` if none exists already. Now every time the
|
||||
app is run you will see the articles created from the previous executions
|
||||
|
||||
### Building the blog
|
||||
|
||||
Run
|
||||
|
||||
```bash
|
||||
v -d use_openssl -o blog -prod . && strip ./blog
|
||||
```
|
||||
|
||||
This will result in a ~400KB binary. `-d use_openssl` tells Veb to link to OpenSSL.
|
||||
Without this flag mbedtls will be embedded, and the binary size will increase to ~700KB.
|
||||
|
||||
|
||||
### To be continued...
|
||||
|
||||
For an example of a more sophisticated web app written in V, check out Gitly: https://github.com/vlang/gitly
|
||||
@@ -0,0 +1 @@
|
||||
blog.db
|
||||
@@ -0,0 +1,13 @@
|
||||
module main
|
||||
|
||||
struct Article {
|
||||
id int @[primary; sql: serial]
|
||||
title string
|
||||
text string
|
||||
}
|
||||
|
||||
pub fn (app &App) find_all_articles() []Article {
|
||||
return sql app.db {
|
||||
select from Article
|
||||
} or { []Article{} }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
drop table if exists Article;
|
||||
|
||||
create table Article (
|
||||
id integer primary key,
|
||||
title text default "",
|
||||
text text default ""
|
||||
);
|
||||
|
||||
insert into Article (title, text) values (
|
||||
"Hello, world!",
|
||||
"V is great."
|
||||
);
|
||||
|
||||
insert into Article (title, text) values (
|
||||
"Second post.",
|
||||
"Hm... what should I write about?"
|
||||
);
|
||||
@@ -0,0 +1,90 @@
|
||||
module main
|
||||
|
||||
import veb
|
||||
import time
|
||||
import db.sqlite
|
||||
import json
|
||||
|
||||
// Context struct must embed veb.Context
|
||||
pub struct Context {
|
||||
veb.Context
|
||||
pub mut:
|
||||
user_id string
|
||||
}
|
||||
|
||||
// App struct for shared data
|
||||
pub struct App {
|
||||
pub mut:
|
||||
db sqlite.DB
|
||||
}
|
||||
|
||||
// Main function
|
||||
fn main() {
|
||||
mut app := &App{
|
||||
db: sqlite.connect('blog.db') or { panic(err) }
|
||||
}
|
||||
sql app.db {
|
||||
create table Article
|
||||
}!
|
||||
// Use veb.run with App and Context types
|
||||
veb.run[App, Context](mut app, 8081)
|
||||
}
|
||||
|
||||
/*
|
||||
// Middleware to run before each request
|
||||
pub fn (mut ctx Context) before_request() bool {
|
||||
ctx.user_id = ctx.get_cookie('id') or { '0' }
|
||||
return true
|
||||
}
|
||||
*/
|
||||
|
||||
// Index endpoint
|
||||
@['/index']
|
||||
pub fn (app &App) index(mut ctx Context) veb.Result {
|
||||
articles := app.find_all_articles()
|
||||
return $veb.html()
|
||||
}
|
||||
|
||||
// New article form endpoint
|
||||
@['/new']
|
||||
pub fn (app &App) new(mut ctx Context) veb.Result {
|
||||
return $veb.html()
|
||||
}
|
||||
|
||||
// Create new article endpoint
|
||||
@['/new_article'; post]
|
||||
pub fn (app &App) new_article(mut ctx Context) veb.Result {
|
||||
title := ctx.form['title'] or { '' }
|
||||
text := ctx.form['text'] or { '' }
|
||||
|
||||
if title == '' || text == '' {
|
||||
return ctx.text('Empty text/title')
|
||||
}
|
||||
|
||||
article := Article{
|
||||
title: title
|
||||
text: text
|
||||
}
|
||||
println('posting article')
|
||||
println(article)
|
||||
|
||||
sql app.db {
|
||||
insert article into Article
|
||||
} or {}
|
||||
|
||||
return ctx.redirect('/')
|
||||
}
|
||||
|
||||
// Get all articles endpoint
|
||||
@['/articles'; get]
|
||||
pub fn (app &App) articles(mut ctx Context) veb.Result {
|
||||
articles := app.find_all_articles()
|
||||
json_result := json.encode(articles)
|
||||
return ctx.json(json_result)
|
||||
}
|
||||
|
||||
// Time endpoint
|
||||
@['/time']
|
||||
pub fn (app &App) time(mut ctx Context) veb.Result {
|
||||
return ctx.text(time.now().format())
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>V Blog</title>
|
||||
</head>
|
||||
<body>
|
||||
user id = @ctx.user_id <br>
|
||||
@for article in articles
|
||||
<div>
|
||||
<b>@article.title</b> <br>
|
||||
@article.text
|
||||
</div>
|
||||
<br>
|
||||
@end
|
||||
<a href='/new'>New article</a>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,13 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>V Blog</title>
|
||||
</head>
|
||||
<body>
|
||||
<form action='/new_article' method='post'>
|
||||
<input type='text' placeholder='Title' name='title'> <br>
|
||||
<textarea placeholder='Text' name='text'></textarea>
|
||||
<input type='submit'>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 103 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 108 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 90 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 90 KiB |
Reference in new issue
Block a user