Skip to content

Commit 88ccffc

Browse files
committed
doc: add guidelines
1 parent 385d109 commit 88ccffc

File tree

4 files changed

+762
-1
lines changed

4 files changed

+762
-1
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ Use the [Conventional Commits](https://www.conventionalcommits.org) format for y
5656
In the PR description, please include a summary of the changes and any relevant context. Also, please make sure
5757
to update the package versions following the [Semantic Versioning](https://semver.org/) rules.
5858

59+
See also: [Code guidelines](doc/code-guidelines.md)
60+
5961
### Releases
6062

6163
The repository has several CI workflows that automatically release new versions of the various components when a new Github release is published.

apps/hermes/server/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ panic = 'abort'
8080
unsafe_code = "deny"
8181

8282
[lints.clippy]
83-
# See https://www.notion.so/pyth-network/Rust-code-guidelines-20a2eecaaac98060b6b2d9a52c9f9141
83+
# See [Rust code guidelines](../../doc/rust-code-guidelines.md)
8484

8585
wildcard_dependencies = "deny"
8686

doc/code-guidelines.md

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
# Code Guidelines
2+
3+
# Language specific
4+
5+
[Rust code guidelines](rust-code-guidelines.md)
6+
7+
# Make your services as resilient to errors as possible
8+
9+
- Perform more benchmarking/add benchmarking tests through our codebase. Currently
10+
there are portions of the codebase that we have unknown performance for that
11+
may become more important as we scale. Most languages have benchmark test
12+
capability, rust has it built in for example.
13+
- Implement error recovery even for unlikely cases. Think about how the service can continue to work after different failures.
14+
- The service should continue to work (if possible) if its dependencies are unavailable or broken.
15+
- Avoid the possibility of leaving the service in a state where it no longer able to start or work properly. The service should be able to recover from things like invalid files or unexpected database state. If that is not possible, provide clear error messages that explain what should be done to fix it.
16+
- Minimize the number of dependencies required for a service to start.
17+
- It should be possible to run multiple instances of each service at the same time.
18+
19+
# Set up essential tooling
20+
21+
- Use strongest lint settings. It is better to have at minimum pedantic warnings
22+
on all projects. Good examples of bad settings: allowing `any` globally in
23+
typescript, ignoring integer clippy type warnings in Rust, etc.
24+
- Add extensive logging, metrics and tracing capability early, much of our code is missing
25+
metrics, good log handling, or ability to do introspection on code that has
26+
failed in retrospect. Good example: hermes launch.
27+
28+
# Keep the code readable and maintainable
29+
30+
- Make heavy use of types to define behaviour. In general introducing a type can be
31+
thought of as introducing a unit test. For example:
32+
33+
```rust
34+
struct PositiveTime(i64);
35+
36+
impl TryFrom<i64> for PositiveTime {
37+
type Err = ();
38+
fn try_from(n: i64) -> Result<Self, Self::Err> {
39+
if n < 0 {
40+
return Err(());
41+
}
42+
return Ok(Self(n));
43+
}
44+
}
45+
46+
```
47+
48+
This can be thought of reducing the valid range of i64 to one we prefer
49+
(given that i64 is the native Linux time type but often we do not want these)
50+
that we can enforce a compile-time. The benefit in types over unit tests is
51+
simply use-at-site of a type ensure behaviour everywhere and reducing the
52+
amount of unwanted behaviour in a codebase.
53+
54+
Currently we do not try hard enough to isolate behaviours through types.
55+
56+
- Avoid monolithic event handlers, and avoid state handling in logic. Some
57+
stateful code in our repos mixes the logic handling with the state handle
58+
code which produces very long, hard to reason about code which ends up as
59+
a rather large inline state machine:
60+
61+
Good:
62+
63+
```tsx
64+
function handleEvent(e, state) {
65+
switch(e.type) {
66+
case Event.Websocket: handleWebsocketEvent(e, state.websockets);
67+
case Event.PythNet: handlePythnetEvent(e, state.pyth_handle);
68+
case ...
69+
}
70+
}
71+
72+
```
73+
74+
Bad:
75+
76+
```tsx
77+
function handleEvent(e) {
78+
// Many inlined state tracking vars. Not much better than globals.
79+
var latstPythNetupdateTime = DateTime.now();
80+
var clientsWaiting = {};
81+
var ...
82+
83+
switch(e.type) {
84+
// lots of inline handling
85+
}
86+
}
87+
88+
```
89+
90+
- Avoid catch-all modules, I.E: `types/`, `utils/`
91+
- Favor Immutability and Idempotency. Both are a huge source of reducing logic bugs.
92+
- State should whenever possible flow top-down, I.E: create at entry point and
93+
flow to other components. Global state should be avoided and no state should be
94+
hidden in separate modules.
95+
96+
Good:
97+
98+
```tsx
99+
// main.ts
100+
function main() {
101+
const db = db.init();
102+
initDb(db);
103+
}
104+
105+
```
106+
107+
Bad:
108+
109+
```tsx
110+
// main.ts
111+
const { db } = require('db');
112+
function() {
113+
initDb(); // Databaes not passed, implies global use.
114+
}
115+
116+
```
117+
118+
- For types/functions that are only used once, keep them close to the
119+
definition. If they are re-used, try and lift them only up to a common
120+
parent, in the following example types/functions only lift as far
121+
as they are useful:
122+
123+
Example File Hierarchy:
124+
125+
```
126+
lib/routes.rs:validateUserId()
127+
lib/routes/user.rs:type RequestUser
128+
lib/routes/user/register.rs:generateRandomUsername()
129+
130+
```
131+
132+
Good:
133+
134+
```tsx
135+
// Definition only applies to this function, keep locality.
136+
type FeedResponse = {
137+
id: FeedId,
138+
feed: Feed,
139+
};
140+
141+
// Note the distinction between FeedResponse/Feed for DDD.
142+
function getFeed(id: FeedId, db: Db): FeedResponse {
143+
let feed: Feed = db.execute(FEED_QUERY, [id]);
144+
return { id, feed: feed, }
145+
}
146+
147+
```
148+
149+
Bad:
150+
151+
```tsx
152+
import { FeedResponse } from 'types';
153+
function getFeed(id: FeedId, db: Db): FeedResponse {
154+
let feed = db.execute(FEED_QUERY, [id]);
155+
return { id, feed: feed, }
156+
}
157+
158+
```
159+
160+
- Map functionality into submodules when a module defines a category of handlers.
161+
This help emphasise where code re-use should happen, for example:
162+
163+
Good:
164+
165+
```
166+
src/routes/user/register.ts
167+
src/routes/user/login.ts
168+
src/routes/user/add_role.ts
169+
src/routes/index.ts
170+
171+
```
172+
173+
Bad:
174+
175+
```tsx
176+
// src/index.ts
177+
function register() { ... }
178+
function login() { ... }
179+
function addRole() { ... }
180+
function index() { ... }
181+
182+
```
183+
184+
Not only does this make large unwieldy files but it encourages things like
185+
`types/` catch alls, or unnecessary sharing of functionality. For example
186+
imagine a `usernameAsBase58` function thrown into this file, that then
187+
looks useful within an unrelated to users function, it can be tempting to
188+
abuse the utility function or move it to a vague catch-all location. Focus
189+
on clear, API boundaries even within our own codebase.
190+
191+
- When possible use layered architecture (onion/hexagonal/domain driven design) where
192+
we separate API processing, business logic, and data logic. The benefit of this
193+
is it defines API layers within the application itself:
194+
195+
Good:
196+
197+
```tsx
198+
// web/user/register.ts
199+
import { registerUser, User } from 'api/user/register.ts';
200+
201+
// Note locality: one place use functions stay near, no utils/
202+
function verifyUsername( ...
203+
function verifyPassword( ...
204+
205+
// Locality again.
206+
type RegisterRequest = {
207+
...
208+
};
209+
210+
function register(req: RegisterRequest): void {
211+
// Validation Logic Only
212+
verifyUsername(req.username);
213+
verifyPassword(req.password);
214+
215+
// Business Logic Separate
216+
registerUser({
217+
username: req.username,
218+
password: req.password,
219+
});
220+
}
221+
222+
```
223+
224+
```tsx
225+
// api/user/register.ts
226+
import { storeUser, DbUser } from 'db/user';
227+
228+
function registerUser(user: User) {
229+
const user = fetchByUsername(user.username);
230+
if (user) {
231+
throw "User Exists;
232+
}
233+
234+
// Note again that the type used here differs from User (DbUser) which
235+
// prevents code breakage (such as if the schema is updated but the
236+
// code is not.
237+
storeUser({
238+
username: user.username,
239+
password: hash(user.password),
240+
});
241+
}
242+
243+
```

0 commit comments

Comments
 (0)