Skip to content
This repository has been archived by the owner on Aug 1, 2022. It is now read-only.

feat(proxy): demo warp based REST API endpoints #250

Closed
wants to merge 81 commits into from
Closed
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
fd37997
Bump proxy dependencies
xla Mar 19, 2020
633ba40
Attempt first end-to-end draft
xla Mar 19, 2020
aa843ef
Move serialisation to http module
xla Mar 20, 2020
20f9943
Merge branch 'master' into xla/220-warp-implementation
xla Mar 21, 2020
7b45e45
Explore json response assertions
xla Mar 22, 2020
a195ce7
Pass state
xla Mar 22, 2020
ef919df
Add docs
xla Mar 22, 2020
da71cc0
Move project endpoints into own module
xla Mar 23, 2020
695b7c9
Flesh out error responses
xla Mar 24, 2020
b9ffe18
Clean up
xla Mar 24, 2020
1f78617
Show case create endpoint
xla Mar 24, 2020
ac73fe1
Implement project registration
xla Mar 24, 2020
dcad433
Merge branch 'master' into xla/220-warp-implementation
xla Mar 25, 2020
2546e50
Test register
xla Mar 25, 2020
41ec0fa
Implement list endpoint
xla Mar 25, 2020
b7e9d0d
Simplify test assertions
xla Mar 25, 2020
ee5fccf
Fix reference
xla Mar 25, 2020
7a02e2f
Merge branch 'master' into xla/220-warp-implementation
xla Mar 25, 2020
87ab5a7
Use String in error fields
xla Mar 25, 2020
02c8c22
Remove explicit path end
xla Mar 25, 2020
dc6a174
Strings are strings are stirngs
xla Mar 25, 2020
76b1737
Use stronger Url type in the core
xla Mar 26, 2020
e4f9a4c
Merge branch 'master' into xla/220-warp-implementation
xla Mar 26, 2020
b947b6a
Wire the api in main
xla Mar 26, 2020
c754fbf
Use 8080
xla Mar 26, 2020
6e21def
Implement notification endpoint
xla Mar 29, 2020
e0a14a8
Wire up some rudimental logging
xla Mar 30, 2020
6bc54c5
chore: document REST API
garbados Mar 31, 2020
db653de
Explore mildly how ts state management could look like
xla Mar 31, 2020
0b84ddb
Merge branch 'master' into xla/220-warp-implementation
xla Apr 1, 2020
1f398bb
Merge branch 'xla/220-warp-implementation' into xla/220-meditations-o…
xla Apr 1, 2020
52f45f7
projects store
sarahscott Apr 2, 2020
a6a6bf1
messages defined outside of event
sarahscott Apr 2, 2020
e667f13
remote data type
sarahscott Apr 3, 2020
238b611
simplify access to readable store
sarahscott Apr 6, 2020
fb9f3e9
modifications to RemoteDataStore
sarahscott Apr 7, 2020
fa8e53c
remove stray TODOs
sarahscott Apr 7, 2020
c7c205f
add another todo for myself
sarahscott Apr 7, 2020
11cd5f7
added more todos
sarahscott Apr 7, 2020
9322187
clean up RemoteDataStore
sarahscott Apr 7, 2020
bbd09ea
Merge branch 'master' into xla/220-warp-implementation
xla Apr 7, 2020
5ee5a4e
Enable identities
xla Apr 8, 2020
4b814ab
Clean up
xla Apr 8, 2020
2f24ac7
Add missing docs
xla Apr 8, 2020
ce15339
Add transaction listing
xla Apr 8, 2020
4ef7506
Add missing docs
xla Apr 8, 2020
a6703e1
Simplify API
xla Apr 8, 2020
d21b9a1
Merge branch 'master' into xla/220-warp-implementation
xla Apr 8, 2020
b437207
Set up linting for ts
xla Apr 9, 2020
e3d5ffe
Improve API access
xla Apr 9, 2020
ea0317b
Set cypress cache directory
xla Apr 9, 2020
be63d25
Attempt to fix CI
rudolfs Apr 9, 2020
53769de
Revert "Attempt to fix CI"
rudolfs Apr 9, 2020
9413bcd
Simplify event handling
xla Apr 10, 2020
cc5bbe4
Make event handling sound
xla Apr 10, 2020
daa7ccd
Pass through project stats
xla Apr 10, 2020
437fc4d
Integrate OpenAPI documentation
xla Apr 14, 2020
741d98e
Minimize docs clutter
xla Apr 14, 2020
fdb953a
Merge branch 'master' into xla/220-warp-implementation
xla Apr 15, 2020
1ec2631
Expose GraphQL and REST API together
xla Apr 15, 2020
54b4455
Merge branch 'master' into xla/220-warp-implementation
xla Apr 15, 2020
4330574
Fix tests
xla Apr 15, 2020
3948265
sourceBrowserStore & parameterized FileSource
sarahscott Apr 16, 2020
500052a
simplify RevisionSelector
sarahscott Apr 16, 2020
751e88e
update revision event
sarahscott Apr 16, 2020
9a97fa7
simplifies FileList
sarahscott Apr 16, 2020
351fc9a
update source path
sarahscott Apr 16, 2020
cf6c842
Implement first source browsing endpoint
xla Apr 16, 2020
ab3f0ed
Use string enum for object type documentation
xla Apr 16, 2020
ca4423f
Adjust types to match proxy
xla Apr 16, 2020
06991c6
Make prettier check happy
rudolfs Apr 16, 2020
cece03b
Correct tye missmatch
xla Apr 16, 2020
88e5854
Fix field
xla Apr 16, 2020
17d6009
Implement commit endpoint
xla Apr 16, 2020
9dd2b79
Implement tree endpoint
xla Apr 17, 2020
d381d07
Implement branches endpoint
xla Apr 17, 2020
63d64e9
Implement tags endpoint
xla Apr 17, 2020
df138a0
Fix docs
xla Apr 17, 2020
07c8538
Complete first circle of source browsing
xla Apr 17, 2020
ebcc13b
blob name and revision event fix
sarahscott Apr 18, 2020
f6c6fef
Merge branch 'master' into xla/220-warp-implementation
xla Apr 20, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions proxy/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 7 additions & 8 deletions proxy/src/coco.rs
Original file line number Diff line number Diff line change
Expand Up @@ -420,18 +420,17 @@ pub fn init_project(
name: &str,
description: &str,
default_branch: &str,
img_url: &str,
img_url: Url,
) -> Result<(git::ProjectId, meta::Project), error::Error> {
let key = keys::device::Key::new();
let peer_id = peer::PeerId::from(key.public());
let founder = meta::contributor::Contributor::new();
let sources = git2::Repository::open(std::path::Path::new(path))?;
let img = Url::parse(img_url)?;
let mut meta = meta::Project::new(name, &peer_id);

meta.description = Some(description.to_string());
meta.default_branch = default_branch.to_string();
meta.add_rel(meta::Relation::Url("img_url".to_string(), img));
meta.add_rel(meta::Relation::Url("img_url".to_string(), img_url));

let id = git::GitProject::init(librad_paths, &key, &sources, meta.clone(), founder)?;

Expand Down Expand Up @@ -477,7 +476,7 @@ pub fn replicate_platinum(
name: &str,
description: &str,
default_branch: &str,
img_url: &str,
img_url: Url,
) -> Result<(git::ProjectId, meta::Project), error::Error> {
// Craft the absolute path to git-platinum fixtures.
let mut platinum_path = env::current_dir()?;
Expand Down Expand Up @@ -564,25 +563,25 @@ pub fn setup_fixtures(librad_paths: &Paths, root: &str) -> Result<(), error::Err
"monokel",
"A looking glass into the future",
"master",
"https://res.cloudinary.com/juliendonck/image/upload/v1557488019/Frame_2_bhz6eq.svg",
Url::parse("https://res.cloudinary.com/juliendonck/image/upload/v1557488019/Frame_2_bhz6eq.svg")?,
),
(
"Monadic",
"Open source organization of amazing things.",
"master",
"https://res.cloudinary.com/juliendonck/image/upload/v1549554598/monadic-icon_myhdjk.svg",
Url::parse("https://res.cloudinary.com/juliendonck/image/upload/v1549554598/monadic-icon_myhdjk.svg")?,
),
(
"open source coin",
"Research for the sustainability of the open source community.",
"master",
"https://avatars0.githubusercontent.com/u/31632242",
Url::parse("https://avatars0.githubusercontent.com/u/31632242")?,
),
(
"radicle",
"Decentralized open source collaboration",
"master",
"https://avatars0.githubusercontent.com/u/48290027",
Url::parse("https://avatars0.githubusercontent.com/u/48290027")?,
),
];

Expand Down
5 changes: 3 additions & 2 deletions proxy/src/graphql/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::convert::TryFrom;
use std::str::FromStr;
use std::sync;

use librad::meta::Url;
use librad::paths::Paths;
use librad::surf;
use librad::surf::git::git2;
Expand Down Expand Up @@ -89,7 +90,7 @@ impl Mutation {
&metadata.name,
&metadata.description,
&metadata.default_branch,
&metadata.img_url,
Url::parse(&metadata.img_url)?,
)?;

Ok(project::Project {
Expand Down Expand Up @@ -368,7 +369,7 @@ impl ControlMutation {
&metadata.name,
&metadata.description,
&metadata.default_branch,
&metadata.img_url,
Url::parse(&metadata.img_url)?,
)?;

Ok(project::Project {
Expand Down
38 changes: 38 additions & 0 deletions proxy/src/http.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//! HTTP API delivering JSON over `RESTish` endpoints.

use librad::paths;
use std::convert::Infallible;
use std::sync::Arc;
use tokio::sync::RwLock;
use warp::{path, Filter};

use crate::registry;

mod error;
mod project;

/// Main entry point for HTTP API.
pub async fn run(librad_paths: paths::Paths, reg: registry::Registry) {
let api = path("v1").and(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be path("api").and(path("v1")), that is, /api/v1, in keeping with typical API (1)(2) namespacing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there is a general convention, best-practice here. Unless I missed an obvious standard body or something similar.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having api distinctly in the path makes sense if the server offers other endpoints as well. For our case the proxy is literally the API itself.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, how about telemetry or debugging/metrics endpoints? Won't we have those somewhere in there?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hypothetically even those metrics and debugging endpoints would be namespaced under /v1/, as /v1/stats in anticipation of a potential /v2/stats or /v2/stats_but_different.

Mastodon and others use a /api/v1/ path because they expose other non-API HTTP endpoints. Twitter puts api in the subdomain rather than the path because the responding server only handles an API, which is more like our use-case as the proxy is the API.

project::filters(librad_paths.clone(), Arc::new(RwLock::new(reg))).recover(error::recover),
);

// TODO(xla): Pass down as configuration with sane defaults.
warp::serve(api).run(([127, 0, 0, 1], 8090)).await;
}

/// State filter to expose the [`librad::paths::Paths`] to handlers.
#[must_use]
pub fn with_paths(
paths: paths::Paths,
) -> impl Filter<Extract = (paths::Paths,), Error = Infallible> + Clone {
warp::any().map(move || paths.clone())
}

/// State filter to expose the [`registry::Registry`] to handlers.
#[must_use]
pub fn with_registry(
reg: Arc<RwLock<registry::Registry>>,
) -> impl Filter<Extract = (Arc<RwLock<registry::Registry>>,), Error = Infallible> + Clone {
warp::any().map(move || Arc::<RwLock<registry::Registry>>::clone(&reg))
}
104 changes: 104 additions & 0 deletions proxy/src/http/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
//! Recovery and conversion of [`error::Error`] to proper JSON responses, which expose variants
//! for API consumers to act on.

use std::convert::Infallible;
use warp::http::StatusCode;
use warp::{reject, reply, Rejection, Reply};

use crate::error;

impl reject::Reject for error::Error {}

impl From<error::Error> for Rejection {
fn from(err: error::Error) -> Self {
reject::custom(err)
}
}

/// Error type to carry context for failed requests.
#[derive(serde_derive::Serialize)]
struct Error {
/// Human readable message to convery error case.
message: String,
/// The triggered error variant.
variant: String,
}

/// Handler to convert [`error::Error`] to [`Error`] response.
pub async fn recover(err: Rejection) -> Result<impl Reply, Infallible> {
let (code, variant, message) = {
if err.is_not_found() {
(StatusCode::NOT_FOUND, "NOT_FOUND", "Resource not found")
} else if let Some(err) = err.find::<error::Error>() {
match err {
_ => {
// TODO(xla): Match all variants and properly transform similar to
// gaphql::error.
(StatusCode::BAD_REQUEST, "BAD_REQUEST", "Incorrect input")
},
}
} else {
(
StatusCode::INTERNAL_SERVER_ERROR,
"INTERNAL_ERROR",
"Something went wrong",
)
}
};
let res = reply::json(&Error {
message: message.to_string(),
variant: variant.to_string(),
});

Ok(reply::with_status(res, code))
}

#[cfg(test)]
mod tests {
use futures::stream::TryStreamExt;
use pretty_assertions::assert_eq;
use serde_json::{json, Value};
use warp::reply::Reply as _;
use warp::Rejection;

#[tokio::test]
async fn recover_custom() {
let have: Value = response(warp::reject::custom(
crate::error::Error::InordinateString32(),
))
.await;
let want = json!({
"message": "Incorrect input",
"variant": "BAD_REQUEST",
});

assert_eq!(have, want);
}

#[tokio::test]
async fn recover_not_found() {
let have: Value = response(warp::reject::not_found()).await;
let want = json!({
"message": "Resource not found",
"variant": "NOT_FOUND",
});

assert_eq!(have, want);
}

async fn response(err: Rejection) -> Value {
let res = super::recover(err).await.unwrap();

let body = res
.into_response()
.body_mut()
.try_fold(Vec::new(), |mut data, chunk| async move {
data.extend_from_slice(&chunk);
Ok(data)
})
.await
.unwrap();

serde_json::from_slice(&body).unwrap()
}
}
Loading