Welcome
Thank you for taking the time to consider contributing to the project!
The aim of this guide is to introduce general concepts that can be taken across many projects in your professional career while directing them specifically at this project.
In this contributing guide, we will cover various topics starting from navigating GitHub issues to submitting a pull request for review.
We advise taking the time to carefully familiarize yourself with the content in each section. Every section has a purpose and it's likely that we will comment on any, if not all, sections in a pull request.
GitHub Issues
GitHub provides a ticketing system called issues which allows users to create and track the progress of tasks that need to be done.
When looking to contribute to a library one of the first places to look is their issues section while following the guidance that the authors of the project have provided in their contributing document.
The issue should describe the work that needs to be done and the contributing document should outline the expectations that must be met in order for the work to eventually make its way into the repository.
Searching for Issues
There are a few points to consider when looking for a task to contribute to. The following sections provide a quickstart guide for navigating the issues in the Sway Libs repository.
Filtering by label
Issues can be grouped into categories through the use of labels and depending on the category a user may choose to contribute to one task or another.
- Is it a bug fix, improvement, documentation etc.
- Is it for the compiler, user interface, tooling etc.
- Is the priority critical and must be resolved immediately or is it a low priority "nice to have"?
Checking for available issues
Once the issues are filtered a task can be selected if another user is not currently assigned to that task otherwise multiple people may be working on the same issue when only one solution can be chosen.
Issue summary
Each issue should have a description which provides context into the problem and what may be done to resolve it.
Filtering by label
The default issues tab shows all of the issues that are currently open. GitHub already provides various search queries that can be made using the search bar however an easier way is to use the labels that the authors have provided to quickly filter for the relevant issues such as bugs, improvements, documentation, etc.
Under the Label
tab you can select any number of labels and any issue that matches those labels will be shown while the other issues will be hidden.
After clicking on the Lib: Cryptography
label the issues have been filtered to show only the issues that have Lib: Cryptography
added to them.
Notice that Lib: Cryptography
is not the only label in the image below. If you wish to further reduce the number of presented issues then additional labels can be added in the same way.
Checking for available issues
It's important to check if anyone else is currently working on an particular issue to avoid performing duplicate work. Not only would this be frustrating but also be an inefficient use of time.
You can check whether someone is assigned to an issue by looking under the Assignee
tab. If there is an icon, then someone is tasked with that issue. If there is no icon, then it's likely that no one is currently working on that issue and you're free to assign it to yourself or post a comment so that the author can assign you.
Issue summary
You can see the activity of an issue by looking at the number of comments. This doesn't really tell you much aside from that there is a discussion about what should be done.
Clicking on the issue near the bottom Sum Merkle Proof Verification
we can see some information about the library with some information on what the library intends to implement and why it is needed.
Creating an Issue
If there is work that a project can benefit from then an issue can be filed via a template or a blank form.
We encourage the use of the provided templates because they guide a user into answering questions that we may have. The templates are not mandatory but they provide structure for answering questions like:
- What steps can be taken to reproduce the issue?
- What feature is missing and how would you like it to work?
- Is the improvement an improvement or a personal nitpick?
The questions themselves are not that important, but what is important is providing as much detail about the task as possible. This allows other developers to come to a decision quickly and efficiently regarding the new issue.
Library Quality
The quality of a library can be determined by a variety of measures such as intended utility or adoption. The metric that the following sections will focus on is developer experience.
In the following sections we will take a look at:
- Library Structure
- The file structure and how easily a library may be navigated
- Code Structure
- How to structure the code inside a file
- Documentation
- How to present information about your library
- Testing
- The file structure and tips for testing
If the library is well structured, tested and documented then the developer experience will be good.
Library Structure
In order to navigate through a library easily, there needs to be a structure that compartmentalizes concepts. This means that code is grouped together based on some concept.
Here is an example structure that we follow for Sway
files in the src
directory.
src/
├── lib.sw
└── my_library/
├── data_structures.sw
├── errors.sw
├── events.sw
├── my_library.sw
└── utils.sw
In the example above there are no directories, however, it may make sense for a project to categorize concepts differently such as splitting the data_structures.sw
into a directory containing individual modules.
data_structures.sw
Contains data structures written for your project.
- structs
- enums
- trait implementations
errors.sw
Contains enums that are used in require(..., MyError::SomeError)
statements.
The enums are split into individual errors e.g. DepositError
, OwnerError
etc.
pub enum MoveError {
OccupiedSquare: (),
OutOfBounds: (),
}
events.sw
Contains structs definitions which are used inside log()
statements.
pub struct WinnerEvent {
player: Identity,
}
my_library.sw
This is the core of your library. It will host anything that is exposed for developers to use with an Application Binary Interface (ABI
) as well as functions contracts or other libraries may call. You can think of this as the entry point which all other devs will use to interact with the library.
utils.sw
Any private functions (helper functions) that your contracts use inside their functions.
Code Structure
Structuring code in a way that is easy to navigate allows for a greater developer experience. In order to achieve this, there are some guidelines to consider.
- Fields in all structures should be alphabetical
- Functions should be declared by the weight of their purity e.g.
read & write
firstread
secondpure
last
- Structures should be grouped into modules and the content inside should be alphabetically ordered
- Dependencies and imports should be alphabetical
- Document the parameters of the interface in alphabetical order and preferably declare them in the same order in the function signature
An important aspect to remember is to use the formatter(s) to format the code prior to a commit.
cargo fmt
to formatRust
filesforc fmt
to formatSway
files
Documentation
Documentation is arguably the most important aspect of any open source project because it educates others about how the project works, how to use it, how to contribute etc.
Good documentation enables frictionless interaction with the project which in turn may lead to a greater userbase, including contributors, which causes a positive feedback loop.
In the following sections we will take a look at how to document the:
- Read me
- The first document a user will see which includes content such as installation instructions
- Code
- Documenting the code itself such that contributors know how to interact with it
- Specification
- Presenting technical (or non-technical) information about your project
Read me
The README.md
is likely to be the first file that a user sees therefore from the perspective of a user there are certain expectations that need to be met.
Introduction
A user needs to know what the library is and what it does. The content in this section should be a brief overview of what the library can do and it should not touch on any technical aspects such as the implementation details.
Once a user has an idea of what they are getting into they can move onto the next section.
Quickstart
The quickstart should inform the user where the library is supported (e.g. the operating system), and has been tested to work, before moving onto the installation and removal instructions.
A user should be able to easily install, use, and potentially remove your library to create a good experience.
Miscellaneous
This "section" can be a number of sections which the authors of the library think the user may be interested in.
Some information may include:
- Links to documents such as contributing guides, blogs, socials etc.
- Ways to support the library
- Known issues
There is a variety of content that may be added, however, it's important to note that this is the first document a user will see and thus should not be overloaded with information. If the user can learn a little about the library, use it, and find links to additional content then the document has achieved its purpose.
Code
Documenting code is an important skill to have because it conveys information to developers about the intention and usage of the library.
In the following sections we'll take a look at three ways of documenting code and a code style guide.
For general documentation refer to how Rust documents code.
ABI Documentation
ABI documentation refers to documenting the interface that another developer may be interested in using.
The form of documentation we focus on uses the ///
syntax as we are interested in documenting the ABI
functions.
In the following snippet, we provide a short description about the functions, the arguments they take, and when the calls will revert. Additional data may be added such as the structure of the return type, how to call the function, etc.
{{#include ../../../../code/connect_four/src/interface.sw:interface}}
In order to know what should be documented, the author of the code should put themselves in the position of a developer that knows nothing about the function and think about what sort of questions they may have.
Comments
Comments are used by developers for themselves or other developers to provide insight into some functionality.
There are many ways to achieve the same outcome for a line of code however there are implementation tradeoffs to consider and a developer might be interested in knowing why the current approach has been chosen.
Moreover, it may not be immediately clear why, or what, some line of code is doing so it may be a good idea to add a comment summarizing the intent behind the implementation.
The following snippet looks at two items being documented using the comment syntax //
.
Item1
has poor comments that do not convey any meaningful information and it's better to not include them at all.Item2
has taken the approach of describing the context in order to provide meaning behind each field
// This is bad. It's repeating the names of the fields which can be easily read
pub struct Item1 {
/// Identifier
id: u64,
/// Quantity
quantity: u64,
}
// This is better. It conveys the context of what the fields are
pub struct Item2 {
/// Unique identifier used to retrieve the item from a vector of items held in storage
id: u64,
/// The number of remaining items left in production
quantity: u64,
}
Naming Components
Documenting the interface and adding comments is important however the holy grail is writing code that is self-documenting.
Self-documenting code refers to code that written in such a way that a regular user who has never seen a line of code before could interpret what it is doing.
One of the most difficult aspects of programming is coming up with meaningful names that describe the content without being overly verbose while also not being too concise.
Naming components is both a skill and an art and there are many aspects to consider such as the context in which that variable may exist. In one context an abbreviated variable may be meaningful because of how fundamental that concept is while in another context it may be regarded as random characters.
Here are some points to consider when coming up with a name for a component.
Abbreviations
Abbreviating names is a bad practice because it relies on contextual knowledge of the subject. It forces the developer to step away from their task in order to find the definition of some abbreviation or perhaps wait on a response from another developer.
On the other hand, common abbreviations may be meaningful for a given context and it may be detrimental to come up with a different, or long form, name to describe the content.
In general, a developer should take a moment to consider if an abbreviation provides more benefit than cost and how other developers may interpret that name in the given context.
That being said, here are some examples that should be avoided.
Single Character Names
Using a single character to name a variable conveys little to no information to a developer.
- Is this a throw away variable?
- What is the variable meant to represent where ever it is used?
- Does it make sense to call it by the chosen character e.g.
x
when referring to formulas?
Ambiguous Abbreviations
A common mistake is to abbreviate a variable when it does not need to be abbreviated or when the abbreviation may be ambiguous.
For example, in the context of an industry that deals with temperature sensors what does the variable temp
refer to?
temperature
temporary
tempo
Perhaps in the specific function it makes sense to use the abbreviation. Nevertheless, it's better to add a few more characters to finish the variable name.
Declarative statements
When choosing a name, the name should be a statement from the developer and not a question. Statements provide a simple true or false dynamic while a variable that may be read as a question provides doubt to the intended functionality.
For example:
can_change
->authorized
- The "can" can be read as a question or a statement.
- Is the developer asking the reader whether something can change or are they asserting that something either is or is not authorized to change?
is_on
->enabled
- "is" can also be read as a question posed to the reader rather than a simple declaration.
Style Guide
Programming languages have different ways of styling code i.e. how variables, functions, structures etc. are written.
The following snippets present the style for writing Sway
.
CapitalCase
Structs, traits, and enums are CapitalCase
which means each word has a capitalized first letter. The fields inside a struct should be snake_case and CapitalCase
inside an enum.
struct MultiSignatureWallet {
owner_count: u64,
}
trait MetaData {
// code
}
enum DepositError {
IncorrectAmount: (),
IncorrectAsset: (),
}
snake_case
Modules, variables, and functions are snake_case
which means that each word is lowercase and separated by an underscore.
Module name:
library;
Function and variable:
fn authorize_user(user: Identity) {
let blacklist_user = false;
// code
}
SCREAMING_SNAKE_CASE
Constants are SCREAMING_SNAKE_CASE
which means that each word in capitalized and separated by an underscore.
const MAXIMUM_DEPOSIT = 10;
Type Annotations
When declaring a variable it is possible to annotate it with a type however the compiler can usually infer that information.
The general approach is to omit a type if the compiler does not throw an error however if it is deemed clearer by the developer to indicate the type then that is also encouraged.
fn execute() {
// Avoid unless it's more helpful to annotate
let executed: bool = false;
// Generally encouraged
let executed = false;
}
Field Initialization Shorthand
A struct has a shorthand notation for initializing its fields. The shorthand works by passing a variable into a struct with the exact same name and type.
The following struct has a field amount
with type u64
.
struct Structure {
amount: u64,
}
Using the shorthand notation we can initialize the struct in the following way.
fn call(amount: u64) {
let structure = Structure { amount };
}
The shorthand is encouraged because it is a cleaner alternative to the following.
fn action(value: u64) {
let amount = value;
let structure = Structure { amount: value };
let structure = Structure { amount: amount };
}
Getters
Getters should not follow the pattern of get_XYZ()
and instead should follow XYZ()
.
// Discouraged style
fn get_maximum_deposit() -> u64 {
MAXIMUM_DEPOSIT
}
// Encouraged style
fn maximum_deposit() -> u64 {
MAXIMUM_DEPOSIT
}
Specification
A specification is a document which outlines the requirements and design of the library. There are many ways to structure a specification and this number only grows when considering the industry and target audience.
For simplicity, a specification can be broken into two levels of detail and the one you choose depends on your target audience.
Non-technical specification
A non-technical specification is aimed at an audience that may not have the expertise in an area to appreciate the technical challenges involved in achieving the goals and thus it can be seen as an overview or summary.
As an example, this may be a developer explaining how a user would interact with the user interface in order to send a coin / asset to someone, without revealing the functionality behind signing a transaction and why a transaction may take some amount of time to be confirmed.
This type of specification is simple so that any layperson can follow the basic concepts of the workflow.
Technical specification
A technical specification is aimed at users that may be regarded as experts / knowledgeable in the area. This type of specification typically assumes the reader understands the basic concepts and dives into the technical aspects of how something works, step-by-step.
Note: Diagrams are a fantastic visual aid no matter the level of detail
Testing
Testing is a large topic to cover therefore this section will only cover some points that are followed in the repository.
File Separation
There are three components to the tests and they have the following structure.
tests/
└── src/
└── my_library/
├── functions/
| └── 1 file per ABI function
├── utils/
| └── mod.rs
└── harness.rs
functions
The functions
directory contains 1 file per function declared in the ABI
and all test cases (not utility / helper functions) for that function are contained within that module.
There are two possibilities with any function call and either the call succeeds or it reverts. For this reason each file is split into two sections:
success
revert
All of the tests where the function does not revert should be contained inside the success
case while the reverting calls (panics) should be contained inside the revert
module.
utils
The utils
directory contains utility functions and abstractions which allow the tests in the functions
directory to remain small, clean and "DRY" (do not repeat yourself).
This can be achieved by separating content into files, internally creating modules in mod.rs
or a mixture of both.
The repository follows the pattern of putting utility functions in mod.rs
and separating them internally into ABI
wrappers and test helpers. The ABI
wrappers are functions which directly call the contract function with the arguments passed in while the test helpers are general utility functions such as creating a new contract instance for a new test etc.
harness.rs
The harness
file is the entry point for the tests, and thus it contains the functions
and utils
modules. This is what is executed when cargo test
is run.
Testing Suggestions
Here are some tips on how to approach testing:
- Similar to code structure content, each file should be ordered alphabetically, with one exception, so that it's easy to navigate
- Test conditions in the order in which they may occur
- If a test has multiple assertions then the first assertion should be tested first, second assertion second etc.
- Test conditions in the order in which they may occur
- Check the code coverage
- All assertions & requirements should be tested
- Check boundary conditions to see if the values passed in work throughout the entire range
- There should be positive and negative test cases meaning that a test should pass with correct data passed in but it should also revert when incorrect data is used
- When writing a test that changes state the test should first assert the initial condition before performing some operation and then testing the outcome of that operation
- If the initial condition is not proven to be what is expected then there is no guarantee that the operation has performed the correct behavior
- This also means that the initial condition should be compared to the post condition
- Comments should only be added to explain sections of each test if they provide insight into some complex behavior
- If a function sets up the initial environment then there is no point in adding a comment "set up the environment" because the function name should be clear enough e.g.
fn setup()
- If a function sets up the initial environment then there is no point in adding a comment "set up the environment" because the function name should be clear enough e.g.
- Any tests that are ignored should be documented in the test so that the reader knows why something is currently unimplemented
- Do not leave in commented out tests.
#[ignore]
them
- Do not leave in commented out tests.
- Unit tests should remain as unit tests
- Do not bundle multiple different checks into one test unless it becomes semantically meaningless when separating them
- Checking that a behavior continues to work more than once may be necessary at times
- If a user can deposit then there should be a test to see that they can deposit more than once
Pull Requests
A pull request is a term used to identify when a piece of work is ready to be pulled into and merged with another piece of work. This functionality is especially useful when collaborating with others because it allows a review process to take place.
In order to create a high quality pull request there are a couple areas that need to be considered:
- Committing your work
- How to incrementally save your work
- Creating a pull request
- How to request a review
Committing your work
A commit
can be thought of as a snapshot in time which captures the difference between the snapshot that is currently being made and the previous snapshot.
When creating a snapshot there are some points to consider in order to keep a high quality log:
- The quantity of work between commits
- Grouping work by task / concept
- The message used to track the changes between commits
Quantity of Work
The amount of work done per commit is dependent upon the task that is being solved however there is a general rule to follow and that is to avoid the extremes of committing everything at once or committing every minor change such as a typo.
The reason for not committing all of the work at once is twofold:
- When a fault occurs which leads to a loss of work then all of that work is lost
- If a section of work needs to be reverted then everything must be reverted
Similarly, small commits should be avoided because:
- A lot of commits may be considered as spam and may be difficult to parse
Categorization
Categorizing commits into issues being resolved allows us to easily scope the amount of work per commit. With appropriate categories the likelihood of too much, or not enough, work being committed is reduced.
An example could be a failing test suite which includes multiple functions that were re-written. In this instance it may be a good idea to fix a test, or a test suite, for one specific function and committing that work before moving onto the next.
This creates a clear separation within the task of fixing the test suites by fixing one suite in one commit and another in another commit.
Commit Messages
Once the issue has been resolved it's time to write a message that will distinguish this commit from any other.
The commit message should be a concise and accurate summary of the work done:
Good commit message:
- Fixed precondition in
withdraw()
which allowed draining to occur
- Fixed precondition in
Bad commit message:
- Fix
- Fixed function
- Fixed an assertion where a user is able to repeatedly call the
withdraw()
functions under an edge case which may lead to the contract being drained
More information about commit messages may be found in:
- The README from joelparkerhenderson
- The Medium article by Apurva Jain
Creating a pull request
There are two types of pull requests and depending on which one is chosen it will convey a different intent to the authors.
A regular
pull request is for when the author of the pull request is satisfied with the work done and believes that the author(s) of the library should perform a review in preparation of merging the work into some branch.
A draft
pull request indicates that work is currently in progress and not ready for review.
When to create a pull request
There are two approaches that can be taken:
- A pull request can be made when the task is deemed to be completed
- A
draft
pull request can be created after the first commit in order to allow for easy tracking of the progress
Which one should be chosen may come down to preference or the contributing guide of a library. That being said, the benefit of creating and working on a draft
is that it makes it easier to spot the request and thus early comments may be left which provide additional support.
How to structure a pull request
Depending on the account permissions and where the pull request is being made, there may be some features that are unavailable. For example, an external contributor may not be able to set a label to categorize the request.
There are at least five sections to consider when creating a pull request:
The Title
It's important to provide a title that accurately identifies the work that is being done. This is easy if there is an issue, even more so if the issue is described well, as the title can be directly copy and pasted from the issue. This allows for a one-to-one mapping of an issue to pull request which makes it easy to spot when an issue is ready to be merged.
The Description
The information in the pull request should be structured neatly through the use of headings, bullet points, screenshots etc. because this makes it easier to immediately see the changes rather than having to parse through one large paragraph.
Some ideas for sections are:
- The changes that have been made and the motivation behind them
- Limitations
- Assumptions
- Future work if the pull request is part of an epic (set of tasks / issues)
The Reviewers
If the library is managed well then a contributor does not have to think about who should review their work because it will be automatically filled in for them. This is done through the use of a code owners file.
If that is not the case then the contributor will need to figure out the correct author(s) for code review and select them (if permissions allow it) or the request will be without any reviews until an author spots the request and assigns someone.
The Labels
If there is an issue which is well managed then the labels for that issue can be set on the pull request (if permissions allow it) otherwise an author may need to set the labels if they choose to.
The Issues
If there is an issue that the pull request is working off of then it's a good practice to reference that issue so that it gets closed automatically when the pull request is merged. This can be done via the user interface or by reference in the description using a closing keyword.
For example, issue number 123
would be referenced in the description as closes #123
.
Additionally, referencing the issue that the pull request is based on allows the reviewer to easily click on the link which will take them to the issue. This makes it easy to see the problem in detail and any discussion that occurred.
Merging the Pull Request
Once the request has received enough approvals from the authors then either the authors or the contributor may merge the work in. When attempting to merge there may be an option to squash the commits. It's a good idea to delete the previous commits in the optional description so that a single message summarizes the entire work that has been done. This makes it easier to parse the commit history.