Tutorial: building and publishing a library¶
Raven packages live on GitHub. There is no central registry and no publish
command: you write a library, push it to a repo, tag a version, and anyone can
depend on it with rvpm add. This tutorial walks the whole loop. We will build
a small but real library, raven-slug, that turns text like "Hello, World!"
into a URL slug like "hello-world", test and document it, publish it, and then
use it from another project.
By the end you will know how a library is laid out, the patterns that make one pleasant to use, and the exact steps to ship it.
If you have not set up a project before, skim the rvpm guide first; this tutorial uses its commands throughout.
Step 1: scaffold the library¶
A library is a package whose entry file is lib.rv at the root. That file is
the API other projects import. Scaffold one with --lib:
You get a small tree:
raven-slug/
rv.toml # the manifest: name, version, edition, dependencies
lib.rv # the API others import
.gitignore # ignores the generated target/ directory
A name like raven-slug is just a convention. Prefixing packages with raven-
makes them easy to spot on GitHub, and the repo name is the last segment of the
import path, so the repo and the package share one name.
Step 2: write the API¶
Open lib.rv and replace the starter code. We will offer two things: a
slugify function for the common case, and a Slugger builder for when someone
needs to change the separator or cap the length. Designing for both is a small
habit that pays off: the easy thing stays a one-liner, and the flexible thing is
there when you need it.
import std/string
// Turn arbitrary text into a URL slug: lowercased, with runs of
// non-alphanumeric characters collapsed to a single separator.
//
// slugify("Hello, World!") // "hello-world"
fun slugify(text: String) -> String {
return Slugger.new().slugify(text)
}
// A configurable slug builder. Start from Slugger.new(), adjust it, then call
// slugify(). Each setter returns a new Slugger, so configurations are cheap to
// reuse and never mutate each other.
struct Slugger {
separator: String,
max_len: Int,
}
impl Slugger {
// A slugger with sensible defaults: "-" between words and no length cap.
fun new() -> Slugger {
return Slugger { separator: "-", max_len: 0 }
}
// Join words with `sep` instead of "-".
fun separator(self, sep: String) -> Slugger {
return Slugger { separator: sep, max_len: self.max_len }
}
// Cap the result at `n` characters. 0 means no limit.
fun max_len(self, n: Int) -> Slugger {
return Slugger { separator: self.separator, max_len: n }
}
// Build the slug for `text`.
fun slugify(self, text: String) -> String {
let lower = text.to_lower()
let out = ""
let pending = false
let i = 0
while i < lower.length() {
let b = lower.byte_at(i)
if _is_alnum(b) {
if pending {
out = out.concat(self.separator)
pending = false
}
out = out.concat(lower.substring(i, i + 1))
} else if out != "" {
pending = true
}
i += 1
}
if self.max_len > 0 && out.length() > self.max_len {
out = out.substring(0, self.max_len)
}
return out
}
}
// True when byte `b` is an ASCII lowercase letter or digit.
fun _is_alnum(b: Int) -> Bool {
let digit = b >= 48 && b <= 57
let letter = b >= 97 && b <= 122
return digit || letter
}
Two things worth calling out:
- Everything is public, except names starting with
_. Raven has nopubkeyword: every top-levelfun,struct,enum, andtraitis part of the package's API. The leading underscore on_is_alnumis a convention that marks it internal;rvpm dochides it, and it signals to readers "don't depend on this." - The builder is immutable. Each setter returns a fresh
Sluggerrather than mutatingself. That means a caller can keep a base configuration around and branch off it safely, which is a friendlier contract than in-place mutation.
Step 3: check that it compiles¶
A library has no main, so rvpm build type-checks it instead of producing a
binary. Run it early and often:
Step 4: write tests¶
A test is a zero-argument function named test_* in a *_test.rv file. It
asserts with std/test, and a failed assertion panics, which the runner reports
as a failure. Put the tests right next to the code, in lib_test.rv:
import std/test { assert_eq_str }
import "./lib" { slugify, Slugger }
fun test_basic() {
assert_eq_str(slugify("Hello, World!"), "hello-world")
}
fun test_collapses_runs() {
assert_eq_str(slugify(" Multiple spaces -- here "), "multiple-spaces-here")
}
fun test_custom_separator() {
assert_eq_str(Slugger.new().separator("_").slugify("a b c"), "a_b_c")
}
fun test_max_len() {
assert_eq_str(Slugger.new().max_len(5).slugify("hello world"), "hello")
}
Run them:
running 4 tests
ok test_basic
ok test_collapses_runs
ok test_custom_separator
ok test_max_len
test result: ok. 4 passed; 0 failed
Each test runs in its own process, so one failure does not take the others down with it. Writing the tricky cases as tests (the leading spaces, the double dashes, the length cap) is also the cheapest documentation you will ever write: they show exactly what the library does.
Step 5: document it¶
Raven has no separate doc-comment syntax. Any run of // lines directly above
an item is its documentation, and rvpm doc collects them into Markdown:
The output lists each public item with its signature and the comment above it:
### slugify
```rust
fun slugify(text: String) -> String
```
Turn arbitrary text into a URL slug: lowercased, with runs of
non-alphanumeric characters collapsed to a single separator.
Because the comments you already wrote in Step 2 are the docs, keeping them
accurate is the whole job. Note that _is_alnum does not appear: internal items
are left out.
Step 6: format¶
One formatter, no options. Run it before every commit so diffs stay about the code, not the whitespace:
rvpm fmt # format everything in the package
rvpm fmt --check # verify formatting in CI; exits non-zero if anything differs
Step 7: write a README¶
The README is the first thing a person reads before adding your package, so it earns its keep. Keep it short and lead with a usage example:
# raven-slug
Turn text into URL slugs. No dependencies.
## Install
```toml
[dependencies]
"github.com/martian56/raven-slug" = "0.1"
```
## Usage
```rust
import "github.com/martian56/raven-slug" { slugify, Slugger }
slugify("Hello, World!") // "hello-world"
Slugger.new().separator("_").slugify("a b") // "a_b"
Slugger.new().max_len(5).slugify("hello world") // "hello"
```
Add a LICENSE file too (MIT is a common, permissive choice). A package with a
clear README and a license reads as something you can trust.
Step 8: publish¶
Publishing is just pushing the repo and tagging a version. There is no upload step and no account to register.
git init
git add .
git commit -m "raven-slug: turn text into URL slugs"
# create the repo on GitHub (via the website, or the gh CLI):
gh repo create martian56/raven-slug --public --source=. --remote=origin
git push -u origin main
Then tag a release. Versions are git tags in vMAJOR.MINOR.PATCH form, and
pushing the tag is the publish:
That is the entire release. The tag is what rvpm resolves a version
constraint against, so once it is on GitHub, the package is consumable.
Step 9: use it from another project¶
From any other package, add the dependency and import it:
That records it in rv.toml, resolves and pins it in rv.lock, and fetches it
into the shared cache. Now import it like any module:
import std/io { println }
import "github.com/martian56/raven-slug" { slugify }
fun main() {
println(slugify("My First Blog Post!")) // my-first-blog-post
}
If a name from your package clashes with one from another, rename it on the way
in. Imports support as for both single names and whole modules:
import "github.com/martian56/raven-slug" { slugify as to_slug }
import "github.com/martian56/raven-slug" as slug // then slug.slugify(...)
Releasing new versions¶
When you change the library, commit, then tag the next version and push the tag. Follow semantic versioning so consumers know what to expect:
- Patch (
v0.1.1): a bug fix, no API change. - Minor (
v0.2.0): new functionality, existing code still compiles. - Major (
v1.0.0): a breaking change, such as renaming or removing a public item.
Consumers stay on the version pinned in their rv.lock until they run
rvpm update, so a new tag never disturbs anyone until they ask for it.
What makes a good Raven library¶
The patterns in this tutorial are the same ones the standard packages follow:
- A one-shot function for the common case, a builder for the rest. Most
callers want
slugify(text); the few who need options reach forSlugger. - Underscore-prefix internals. It keeps the public surface small and the generated docs clean.
- Immutable builders. Setters that return a new value compose safely and read clearly.
- Tests beside the code. They guard against regressions and double as examples.
- A README that opens with usage, a
LICENSE, and doc comments on every public item.
Keep the API small, name things plainly, and let rvpm fmt, rvpm test, and
rvpm doc do the busywork. That is the whole craft.