- Use Unix line endings (single LF character).
- Use 4 spaces for indentation.
- Use only spaces for indentation (no tabulation).
- Preferred maximum line width is 140 characters, but try to keep lines width at most 120 characters (we all have large monitors these days, so no need to stick to old 80 columns rule).
- Try to follow Rust coding conventions in all other cases. Compiler will warn you when you don't follow simple rules for constants, traits and methods naming, just follow its recommendations.
- Use
rustfmt
to format code from time to time (preferable way is to runcargo fmt
).
Also, the project has .editorconfig
file with basic coding style definitions
readable by most IDEs and editors. Please look at editorconfig.org for
details and find a plugin for your editor of choice in
the download section. If you install this plugin into your IDE/editor,
it will setup indentation, end-of-lines and other basic text formatting options
to conform the coding conventions described above. It will save you from many
(mis)formatting problems and help keep code clean and tidy.
You will need stable Rust 1.6.0 or later. I follow stable Rust channel, but try to check it on nightly channel to make sure the project is future proof.
You will also need Cargo 0.7.0 or later. It comes with Rust 1.6.0.
You can download Cargo and Rust from the Rust download page.
I recommend you to install rustfmt for code formatting. Install it with
cargo install rustfmt
. Make sure you have the directory Cargo installs
binaries in is in your PATH
. After this, you can use cargo fmt
command in
the project directory to format code according to code conventions.
And of course you will need Git. No excuses here, you must know Git basics in order to work with Github.
Fork the main project repo into your Github account, then clone it and add upstream remote to keep it in sync with upstream:
git clone [email protected]:$USER/vkrs.git
cd vkrs
git remote add upstream https://github.com/kstep/vkrs
OK, you are all set now!
To start contribution, you will need to find some task to do. Visit the
project issues page and look for unassigned issues with s-help wanted
label.
Find some issue you are interested in and comment on it to let me know you want to take it. I will assign it to you and will try to guide you in your first steps to contribution. It's OK if you don't know Rust, or systems programming, or some tricky Git workflow, whatever it is, I'll be happy to help you, answer your questions if I can, or introduce you to forums and chats where more wise people will be able to help you, if I can't.
Once you have your first task chosen, create a branch in your repository. You
may name it however you want, just make sure its name somehow reflect the issue
problem in one-to-three short words. It's also a good idea to place issue
number in the branch name, e.g. issue-123
, as it will help everybody keep
track of your progress on the task:
git checkout -b issue-123-fix-bug
Then you are free to hack. Commit often, try to keep your commits messages meaningful, your commits small and try to place logically related changes into distinct commits, while keeping unrelated changes in different commits.
Once you are done, it's a good idea to make sure your code is formatted
according to coding conventions by running rustfmt
on it:
cargo fmt
git commit -a -m "rustfmt"
And then push your branch to Github:
git push origin -u issue-123-fix-bug
Then create a Pull Request to let me know you are ready to merge your changes. I'll review your commits, and if they are all OK, I'll merge them. If not, I'll try to point you to things you may need to change. It may take several iterations and new commits to make your changes fit the project, but once everything is fine, PR will be merged.
After successful merge, you are safe to remove the issue branch:
git branch -d issue-123-fix-bug # remove local branch
git push origin :issue-123-fix-bug # remove remote branch
Don't forget to update your fork to the upstream after this:
git checkout master
git pull --rebase upstream master
git push origin master
Then you are ready to take on the next issue. Good luck!
This section contains in depth discussion on project architecture and VK API shape. It may seem too long and complex, so you may want to skip it and learn the project architecture the hard way by jumping in directly to contribution, but try at least skim it to have some common overview of the project shape.
The project is about VK API implementation in Rust. The VK API is quite large, so one need to keep to some strict conventions to avoid confusion (not to say "avoid to getting crazy").
All API requests are made with either GET or POST HTTP methods, interchangeably. Current implementation always uses POST requests, but it may change in the future. OAuthy authorization is used, and some requests require certain permissions and an access token, while others require neither.
Request bodies are always www-form-urlencoded strings, responses are always JSON encoded (they may be XML encoded by client request, but this feature is never used in the client implementation).
Request bodies strictly define response structure, so once request is known, we can expect some definite JSON structure in response.
The VK API is also versioned, API version is passed in a v
argument in any
request and may affect the response structure. So each request is also marked
with some API version.
So the client API as implemented in this library revolves around two concepts:
- Requests, which define API method to call and arguments be sent to the server,
- Responses, which define server responses shape.
Each API request is represented with some struct
, which implements Request
trait. Request structs keep fields to represent query to be sent. They also
define expected response type.
Each API response is represented with some struct
, which implements
serde::de::Deserialize
trait.
Request structures follow inline builder pattern, and define at least new()
constructor method without arguments, which sets up sensible defaults in the
fields.
So in order to implement some API method, one must do the following:
- create request structure type,
- implement builder pattern on it (that is an
fn new() -> Self
method and setter methodsfn field_name(&mut self, value: T) -> &mut Self
for each field), - create response structure type,
- implement
Request
trait on request type (i.e. setResponse
associated type to the response type, implementfn to_query_string(&self) -> String
method to build query string, implementfn method_name() -> &'static str'
method which must return method name (e.g."audio.get"
), and optionally implementfn permissions() -> Permissions
method which returns permissions set required by the request). - and then implement
serde::de::Deserialize
trait for the response type.
That sounds like a lot of work, but hold on! There is some remedy to it ahead.
This section is subject to change, as the project is still evolving, so it may lag behind real project structure. In case of confusion, take real code layout for guidance.
So, if you followed through previous section, you now have some high level vision of the project's architecture. Now let's map it to actual code.
I use serde and serde_json to deserialize responses. To avoid boilerplate code I use code generation with macros. I use syntex to generate code in stable Rust.
Fear not, my friend, you won't have to worry about it very much, as the framework is ready, and all you need to do, is just follow some simple steps.
So main layout is simple:
- all common API related things are already in
api
module (api.rs
andapi.rs.in
files), you won't need to touch it very much, if at all, but you will need to use some parts of it often; - all main API parts go to a module with the same name, as the API part,
e.g. audio API go to
audio
module (audio.rs
andaudio.rs.in
), photos API go tophotos
module (photos.rs
andphotos.rs.in
), etc.;
You may notice, that modules come in pairs: *.rs
and *.rs.in
files. The
*.rs.in
files are processed with serde_codegen code generator during build
process, so all response types go to *.rs.in
files, so serde_codegen can
generate Deserialize
trait for them.
Request types and all helper types which doesn't come with response (and don't
require Deserialize
trait implementation) go to *.rs
files.
To join up the files during compilation, you will need to place the following
snippet after use
clauses into your *.rs
file:
#[cfg(feature = "unstable")]
include!("filename.rs.in");
#[cfg(not(feature = "unstable"))]
include!(concat!(env!("OUT_DIR"), "/filename.rs"));
Use your actual *.rs
file name instead of filename.rs
.
Now to the naming conventions:
- API parts are put into modules with the names matching API part name
(
audio
,photos
,market
, etc.). - Request types are named after API method names without API part name
prefix (
audio.get
→Get
,utils.getServerTime
→GetServerTime
, etc.). - Response types are named after return object names (
User
,Photo
,Audio
). If response type name conflicts with request type name in the same module (e.g.Post
method (as inwall.post
) andPost
response object), add module prefix to the response type name (WallPost
). - Some request or response fields may have names matching Rust keywords, most
notable such name is
type
. In such a case, for response type field rename it to some sensible synonym (type
is renamed tokind
by convention) and use#[serde(rename="orignal_name")]
attribute to set real response field name. For request fields, you will use extendedrequest!
orrequest_ref!
macro with aliases (more on this later). If in doubt about good synonym, search project for original field name and take already used synonym to keep field names consistent.
So, first things first. You need to find or create a module for the API method to place. If you read the previous section carefully, you now know where to place all these things. Let's say you want to implement audio.search method.
You need to find (or create if they're not there) the following files:
src/audio.rs
for requests and other main parts,src/audio.rs.in
for responses to be processed with serde_codegen (so you don't to implement Deserialize trait manually).
In our case the files are already here. But what if they would be already in
place? Well, you will need to create them. Let's leave audio.rs.in
empty for
now. Now place the following snippet into audio.rs
file:
#[cfg(feature = "unstable")]
include!("audio.rs.in");
#[cfg(not(feature = "unstable"))]
include!(concat!(env!("OUT_DIR"), "/audio.rs"));
This snippet will join up both files into single module during compilation.
Place all imports (use
clauses) above this code, and all other things below
this code.
Now add the following line into lib.rs
file in order to make this module visible:
pub mod audio;
Congratulations! You have just set up basic module structure.
Now to the fun part.
As mentioned above you will need to do the following to implement VK API method:
- create request structure type (this will be in
audio.rs
in our example), - implement builder pattern on it (to
audio.rs
), - create response structure type (this will go to
audio.rs.in
), - implement
Request
trait on request type (also inaudio.rs
), - implement
serde::de::Deserialize
trait for the response type (will be done automagically with code generation framework).
Just one more thing before we continue.
Note, also, I try to implement as much standard traits on public types as
possible. Usual minimum I implement via #[derive()]
is Debug
, PartialEq
and Clone
. Also, if possible, I implement Copy
(if all type fields are
copyable) and Eq
(almost always, except for some rare cases when a type
contains something like floating number fields (f32
or f64
) which are not
Eq
). However, it's all about putting correct #[derive()]
clause before type
definition, so hustle is minimum. Here's a snippet:
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
Just put it before every request and response type and remove from it traits compiler complains about.
Requests implementation is made really simple due to two wonderful macros I
prepared for you. Behold request!
and request_ref!
Both of them define
simple DSL to describe all aspects of VK API method request, that is: method
name, hardcoded fields (like version field v
), argument fields, response type
and required permissions.
The two macros are very similar, the only difference is request_ref!
allows
you to put fields with reference types into your generated request structure
(hence the suffix _ref
), while the request!
macro generates a simple
structure type without references and lifetimes.
Now to the code:
request_ref! {
#[derive(Eq, Copy)]
struct Search for ["audio.search"](v => 5.44) -> Collection<Audio> [Audio] {
sized {
auto_complete: bool = () => {bool},
lyrics: bool = () => {bool},
performer_only: bool = () => {bool},
sort: Sort = (Sort::Popularity) => {AsRef},
search_own: bool = () => {bool},
offset: usize = (0) => {},
count: usize = (30) => {},
}
unsized {
q: str = ("") => {=},
}
}
}
This snippet shows of almost all main aspects of the request_ref!
macros. We
use _ref
version here because of string field q: &str
. We could use
request!
macro if we chose q
to be String
, but we don't want to make our
users allocate strings for us, so we will just borrow the string. Now let's go
through the DSL step by step.
First we define some custom derives (#[derive(Eq, Copy)]
). Note the macro
will add derived implementations for PartialEq
, Clone
and Debug
traits
for us, these two traits a left out because some types may contain not-Copy
field types (like String
) and/or non-Eq field types (f32
). In our case
there are no such fields, so we just go on and out-in for these useful derives.
Remember, if we don't do it, our library users won't be able to do it for us
due to orphan trait implementation rule.
Then we define a struct in usual way. We don't mark it as pub
lic, macro will
do it for us. We name our structure Search
according to our conventions
(remember we are defining audio.search
method, and audio
part is
already encoded in our module name). Also, we define this struct for ["audio.search"]
with hardcoded argument (v => 5.44)
. We could move this
v
into struct fields and let user change version number in runtime, but
it will ruin our queries flow, because version defines response type, so if
we give user this freedom to choose API version, we will lose our freedom
to set response type structure. So we hardcode it in our implementation.
The same thing can have sense for other fields, e.g. exteded
fields for in
requests change the available fields drastically, redefining response structure
in a very different way, so we will hard code such structure defining fields.
Now we set the response type to -> Collection<Audio>
. The generic
Collection<T>
type resides in api
module so you will have to put use api::Collection;
to the top of your module. This type defines basic collection
response layout and contains count
and items
fields. You will see and use
it very often, actually almost always when a method returns some collection
of objects. If our method were to return single object, we would write
-> Audio
here instead.
After it, we see [Audio]
slice-like thingy. It's a permission definition
block. It can actually contain a comma-separated list of permission names, as
defined in enum api::Permission
type. In our case the audio.search
method
requires "audio" permission, defined by api::Permission::Audio
variant. If
our method required several permissions, e.g. both "audio" and "video", we
would write [Audio, Video]
here. This block can be empty or omitted
altogether for public methods which doesn't require authorization and/or
special permissions, like utils.getServerTime
.
Now we go to the field definition block wrapped in the curly braces {}
as a
simple struct type. Let's ignore sized {}
and unsized {}
blocks for now and
go straight to field definitions.
Fields are defined in a way very similar to usual struct fields definition, but with some extra meta info stuck in. Let's look at it closer:
auto_complete: bool = () => {bool},
First comes field name and type, no surprises here. Then we have an equal sign,
very similar to variable assignment syntax. The expression in round brackets
after it defines the default for the field. It may be some value of correct
type, e.g. (true)
or (false)
in this case. Empty pair of brackets means
"default value" for the given type, and is a synonym to (Default::default())
.
Then we have a fat arrow and an expression in curly braces. This syntax defines
an expression to map the field value to some &str
value, which will go to
query string. The value in curly braces can be one of the following special
keywords, which define some common mapping pattern:
{}
(empty braces) — the same as{ToString}
.{ToString}
— convert value into a string withString::to_string()
method. Not very efficient, so not recommended for string fields, but works well for numeric fields.{Borrow}
— convert value into a&str
with aBorrow::borrow()
method. The field type must implementBorrow<str>
trait. Very efficient forString
fields, should be the default for them.{AsRef}
— convert value into a&str
with aAsRef::as_ref()
method. As efficient, as{Borrow}
above. The field type must implementAsRef<str>
trait. My usual choice for different enum types, for which I just implementAsRef<str>
which returns hard coded&'static str
for each given variant.{bool}
— for boolean fields only. If boolean field value is true, produces"1"
value, otherwise"0"
.{AsRef<Vec>}
— the same asAsRef
above, but for iterable fields (of type&[T]
orVec<T>
), where collection elements implementAsRef<str>
. This mapper will iterate over the collection, converting each element into&str
withAsRef::as_ref()
, and then join up all strings into a single one with a comma as a glue.{AsRef<Option>}
— forOption<T: AsRef<str>>
fields. If the field value isNone
, it will convert it to the empty string (""
), otherwise it works just like{AsRef}
above.{Vec}
— the same as{AsRef<Vec>}
above, but usesToString::to_string()
instead ofAsRef::as_ref()
to convert collection elements into strings.{Option}
— the same as{AsRef<Option>}
above, but usesToString::to_string()
instead ofAsRef::as_ref()
to convert collection elements into strings.{=}
— pass the field value intact. The field must already be of type&str
.
Also, it can have closure-like syntax like this:
album_id: Option<Id> = () => { |value|
value.as_ref().map(ToString::to_string).as_ref()
.map(Borrow::borrow).unwrap_or("")
},
In this case the mapper block contains variable name in pipes |value|
, which
will have &T
type, where T
is the type of the field (&Option<Id>
in this
concrete case), and then an expression, which must transform this variable into
a &str
.
Now to the sized
and unsized
blocks. These blocks are special to
request_ref!
macro, the request!
macro has field definitions list directly
in its fields block:
request! {
#[derive(Copy, Eq)]
struct GetLyrics for ["audio.getLyrics"](v => 5.44) -> Lyrics [Audio] {
lyrics_id: Id = () => {}
}
}
(Try to read this DSL as a home work, it shouldn't be difficult after you have gone through all the words above.)
As block names say, they define sized and unsized request fields. The field
declaration syntax in them are the same, except for one small but important
detail: in unsized {}
block all field types can (but don't have to) have
unsized types, like str
or [T]
. The macro will prepend these types with
reference prefix &
and a lifetime specifier, bound to the parent structure.
That means the following definition:
q: str = ("") => {=},
Will generate the following field in our Search
struct:
q: &str,
One last thing to mention. If request_ref!
structure contains only "unsized"
fields, you can omit sized
and unsized
blocks and just put fields
definitions directly in fields definition block. E.g. if our Search
struct
contained only q
field, we could write it down this way:
request_ref! {
#[derive(Eq, Copy)]
struct Search for ["audio.search"](v => 5.44) -> Collection<Audio> [Audio] {
q: str = ("") => {=},
}
}
So this would generate the following structure type:
#[derive(PartialEq, Debug, Clone, Eq, Copy)]
pub struct Search<'a> {
q: &'a str,
}
Now we are done with request. So, why all this hustle with custom DSL, you would ask? Well, let me show you the code again:
request_ref! {
#[derive(Eq, Copy)]
struct Search for ["audio.search"](v => 5.44) -> Collection<Audio> [Audio] {
sized {
auto_complete: bool = () => {bool},
lyrics: bool = () => {bool},
performer_only: bool = () => {bool},
sort: Sort = (Sort::Popularity) => {AsRef},
search_own: bool = () => {bool},
offset: usize = (0) => {},
count: usize = (30) => {},
}
unsized {
q: str = ("") => {=},
}
}
}
This sixteen lines have just generated the following things for us:
- The
struct Search<'a> { ... }
with 8 fields (it would take 11 lines of code including derive attribute). - The
Struct::new()
constructor which set all fields to sensible defaults we just provided in the DSL (another ~12 lines of code). - A setter method for each field (at least 3 lines of code per field, which amounts to 24 lines).
- And
Request
trait implementation (~7 lines + at least 1 line per field, that is about 15 lines of code, if you write short methods in one line, and more if you format them nicely).
This amounts to 62 simple lines of code, without comments and empty lines, using one-liner methods, vs. 16 lines of macro invocation.
Yes, the DSL is very dense, but it describes exactly what request does in a reasonable enough way, so I bet you already can read all (or almost all) request definitions in the project, and even compose several yourself.
If a request field name is equal to a Rust keyword, you will have to choose
another field name in the struct type. To do it, use the extended request!
DSL syntax with field aliases. For now, if you set an alias for at least one
field in a structure, you must set aliases for all fields. This not very
convenient, and I know it, but it's a macro system limitation I
haven't found a way to work around yet.
For example you have a request with a type
field name, and you decided
to name it kind
in your struct type. Here's how you can do it:
request! {
struct Report for ["users.report"](v => 5.44) -> Bool {
user_id as ("user_id"): Id = (0) => {},
kind as ("type"): ReportKind = (ReportKind::Spam) => {AsRef},
}
}
Note, even if other fields are valid Rust identifiers (the user_id
field
here), you must use alias syntax user_id as ("user_id")
for them as well.
This may change in the future if I find a way to work around this issue.
This one is even simpler. You just take response JSON and write it down as
pub struct Something {}
into *.rs.in
file (that would be audio.rs.in
in
our example). And you will use this Something
after arrow ->
in some
request definition DSL clause. Also remember to make the struct type and all
its fields pub
lic.
Then you just put the following snippet right above your new structure type definition:
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
And you are done.
Note the Deserialize
trait (which is required for response types!). Also,
note that it doesn't include Copy
trait, as responses usually contain at
least one String
field, which is not copyable. If your response doesn't have
non-copyable fields, add Copy
to derived traits as well. And yes, if your
response contains non-Eq fields (like floats), remove Eq
from derive
attribute.
If a response field name is equal to a Rust keyword, you will have to choose
another field name in the struct type. To do it, add #[derive(rename)]
attribute to the field. For example, if a response contains type
field, you
may rename it to kind
this way:
#[derive(Debug, PartialEq, Eq, Copy, Clone, Deserialize)]
pub struct Object {
#[serde(rename="type")]
pub kind: u32,
}
Now to the last point. This will be really simple, I promise.
There are some helpful types around this library, you will need to know about in order to use them instead of reinventing a wheel.
Some types I already mentioned above, e.g. api::Collection<T>
and
auth::Permission
.
There are also several types and type aliases which add some semantic meaning
to response and request fields. You should use them instead of simple types
where appropriate. I.e. if you have some user_id
field, don't just set its
type to u64
mindlessly, use api::Id
type instead. It will make the code
more semantic and will save some hustle in the future if I want to change type
aliases to some more complex types.
Here's the full list:
api::Collection<T: Deserialize>
. You already met it above. Almost all collections, which come from VK, are of this type. Remember, it has two fields:count
(with the total number of items in the collection) anditems
(just aVec<T>
, which contains some subset of the items, cut from the total collection with, for example,offset
andcount
method arguments (for "audio.search" and many other ".get" and ".search" methods)).auth::Permission
— describes all available API permissions. You won't normally modify it, just read it to know what permissions you can put into request permissions definition block.auth::Permissions
— just a compact set of permissions. Just know it exists, and don't worry about it. You will rarely use it anywhere, it's more for library users, than contributors.api::OwnerId
,api::Id
,api::FullId
— different kinds of ids.OwnerId
is the type forowner_id
fields, which can be negative for group ids and positive for user ids (hence it's just an alias toi64
now). All other*_id
fields should haveId
type (a.k.a.u64
). TheFullId
type is usually used in requests and represents a combination of owner id and object id, that is it's just an alias to(OwnerId, Id)
tuple for now.api::Timestamp
— a date-time fields type.api::Duration
— a duration type.api::Bool
— a boolean type, for use in responses only. It's just an alias tou8
now, as VK returns 1/0 instead of true/false. Maybe it will be removed in the future.api::ErrorCode
— this one may be important for you. It's an enum type which enumerates all possible API error codes. If you meet some new error codes in API method description, and it's missing from this enum, please add it into it. See below for details.api::Error
— a common library errors enum. For library users. Just know it exists.api::Result
— a convenience alias toResult<T, Error>
, the main return type from the request. For library users, so just know it exists.
There's also one helpful macro you may use: enum_str!
. It's a common pattern
throughout the library to define an enum type of possible field values, and
then implement AsRef<str>
on it to convert it to string for API interaction.
To avoid code duplication and boilerplate you may use enum_str!
to do both of
the things in one run:
enum_str! { Service {
Email = "email",
Phone = "phone",
Twitter = "twitter",
Facebook = "facebook",
Odnoklassniki = "odnoklassniki",
Instagram = "instagram",
Google = "google",
}}
This will create a pub enum Service
with a bunch of common derived traits
(Debug
, Clone
, Copy
, PartialEq
and Eq
) and an AsRef<str>
implementation, so you can use this type with {AsRef}
converter in request!
macros.
The enum api::ErrorCode
type described above contains a full list of VK API
error codes, including error descriptions in its Display
trait implementation.
It's quite comprehensive, but some methods return very specific error codes
(e.g. wall.post method). If you ever meet some method, which defines custom
error codes, please look at the ErrorCode
type, and if the codes are missing
from it, you will need to add them into the enum.
Please try to keep enum variant names short, but concise and meaningful. The longest error code variant name is 26 characters long, please don't try to beat this record! Use shorter names. Use existing variant names as a guidance.
If you add new error code into ErrorCode
type, you will also need to add it
to From<u32>
, Into<u32>
and Display
traits implementations for
ErrorCode
. Place short and meaningful text description in Display
trait
implementation for the new codes. A good default is to just take the error
description from VK docs page and put it into f.write_str()
in lower case.