Skip to content

Commit 39f7cac

Browse files
committed
init
0 parents  commit 39f7cac

38 files changed

+1125
-0
lines changed

.cargo_vcs_info.json

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"git": {
3+
"sha1": "842506c917b644aba1aa26f4ca7b48c7c4d6b711"
4+
},
5+
"path_in_vcs": ""
6+
}

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
resources/
2+
target/

Cargo.toml

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
2+
#
3+
# When uploading crates to the registry Cargo will automatically
4+
# "normalize" Cargo.toml files for maximal compatibility
5+
# with all versions of Cargo and also rewrite `path` dependencies
6+
# to registry (e.g., crates.io) dependencies.
7+
#
8+
# If you are reading this file be aware that the original Cargo.toml
9+
# will likely look very different (and much more reasonable).
10+
# See Cargo.toml.orig for the original contents.
11+
12+
[package]
13+
edition = "2021"
14+
name = "perspective_api"
15+
version = "1.0.0"
16+
authors = ["honestlysamuk"]
17+
exclude = ["src/main.rs"]
18+
description = "An unopinionated client and a somewhat opinionated service for the Perspective API."
19+
homepage = "https://github.com/honestlysamuk/perspective_api"
20+
documentation = "https://developers.perspectiveapi.com/s/?language=en_US"
21+
readme = "README.md"
22+
license = "BSD-3-Clause"
23+
repository = "https://github.com/honestlysamuk/perspective_api"
24+
25+
[dependencies.anyhow]
26+
version = "1.0.86"
27+
optional = true
28+
29+
[dependencies.reqwest]
30+
version = "0.12.5"
31+
features = ["json"]
32+
33+
[dependencies.serde]
34+
version = "1.0.205"
35+
features = ["derive"]
36+
37+
[dependencies.serde_json]
38+
version = "1.0.122"
39+
40+
[dependencies.thiserror]
41+
version = "1.0.63"
42+
43+
[dependencies.tracing]
44+
version = "0.1.40"
45+
46+
[dependencies.url]
47+
version = "2.5.2"
48+
49+
[dev-dependencies.pretty_assertions]
50+
version = "1"
51+
52+
[dev-dependencies.tokio]
53+
version = "1"
54+
features = ["full"]
55+
56+
[features]
57+
service = ["anyhow"]
58+
59+
[lints.clippy]
60+
collapsible_match = "warn"
61+
expect_used = "warn"
62+
match_bool = "warn"
63+
match_ref_pats = "warn"
64+
match_same_arms = "warn"
65+
match_single_binding = "warn"
66+
needless_bool = "deny"
67+
needless_late_init = "warn"
68+
needless_match = "warn"
69+
redundant_guards = "warn"
70+
redundant_pattern = "warn"
71+
redundant_pattern_matching = "warn"
72+
single_match = "warn"
73+
single_match_else = "warn"
74+
unwrap_used = "warn"

Cargo.toml.orig

+45
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

LICENSE

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
BSD 3-Clause License
2+
3+
Copyright (c) 2024, honestlysam
4+
5+
Redistribution and use in source and binary forms, with or without
6+
modification, are permitted provided that the following conditions are met:
7+
8+
1. Redistributions of source code must retain the above copyright notice, this
9+
list of conditions and the following disclaimer.
10+
11+
2. Redistributions in binary form must reproduce the above copyright notice,
12+
this list of conditions and the following disclaimer in the documentation
13+
and/or other materials provided with the distribution.
14+
15+
3. Neither the name of the copyright holder nor the names of its
16+
contributors may be used to endorse or promote products derived from
17+
this software without specific prior written permission.
18+
19+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

README.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# perspective_api
2+
An unopinionated client and a somewhat opinionated service for the Perspective API.
3+
4+
Activate the service feature for some extra convenience.
5+
6+
Version 1.0.0 is released now. I have aimed at an MVP to begin. There is another endpoint not yet implemented, and a command line version is also not yet implemented.

Ticket/20240808.md

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# 0800
2+
3+
Read through all code, familiarise myself with the task.
4+
Summary:
5+
There is an integration with the Perspective API built in Typescript which still needs migrating to Rust. There is a sample implementation for the TikTok API for reference, so I can copy the style. The MVP is to port one endpoint from Typescript into Rust in a new client.
6+
7+
Strategy:
8+
1. I start low-level by creating some requests in [Bruno](https://www.usebruno.com/). I can't implement a client without exploring the API outside the clutter of a programming language.
9+
2. Capture request and response JSON and use it to generate structs and for serde tests.
10+
3. Write `reqwest`-level modules for calling the endpoint.
11+
4. Integrate this module with a service.
12+
13+
(Copied from the readme)
14+
15+
My usual first step with a port is to have an example of the old client functional so I can compare implementations.
16+
17+
To begin with, I will take point one and explore Perspective with Bruno.
18+
19+
# 0841
20+
21+
The perspective API already has Python and Node.js implementations provided by Google. There is also a [sample Typescript implementation](https://github.com/conversationai/perspectiveapi-simple-server/tree/main).
22+
It requires a Google Cloud account and a verification process to use properly, which takes up to one business day. This
23+
could be step 4.
24+
25+
# 0941
26+
27+
The "old client" just calls the actual JS API client and adds an API key etc to make the internal interface more pleasant.
28+
Is the challenge therefore to rewrite the typescript wrapper for this API in Rust, leveraging the FFI to call that
29+
JS package through WASM, or is it to rewrite that JS client itself?
30+
31+
# 1038
32+
33+
My first coding task will be to model the typescript functionality in Rust, without considering IO for now.
34+
Bruno is a great tool, but I am struggling to get the response back. Testing with cURL works, but what looks
35+
like the same configuration does not work in Bruno. I will revisit this after some modelling.
36+
37+
I have fleshed out the project with some copy/pasting from the other two projects. I have modelled the structure
38+
on the TikTok project, and I am working through the typescript project in parallel to form a first case.
39+
40+
# 1200
41+
42+
Lunch
43+
44+
# 1300
45+
46+
Got Bruno to work. Silly auth config issue. Now I can flesh out some examples and decide on the API contract.
47+
48+
TODO
49+
50+
Build a generic Rust library of one function called analyze which mimics the functionality of the original JS API.
51+
Flesh out a collection of Bruno examples demonstrating the use of the single end point.
52+
Write tests which call the endpoint and demonstrate its use.
53+
Write a Stub implementation.
54+
55+
Build a specific Arwen library of one or more functions which mimics the old typescript implementation and uses the generic library.
56+
Build a service using the Arwen library using Axum
57+
58+
59+
# 1430
60+
61+
Almost got a reqwest client to work with the sample data I used in Bruno.
62+
63+
# 1500
64+
65+
Conversation with James to focus the scope of the work. In the same crate, I am to build a mirror of the Perspective API
66+
in one module, and a service for Arwen using that API in another.

Ticket/20240809.md

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# 1808
2+
3+
Successfully called the endpoint from Rust.
4+
Observations:
5+
- You can input the body with something which serializes into json.
6+
- You can input the params with the .form() method, instead of formatting
7+
the URL directly.
8+
- The .form method appears not to work well with other helper methods.
9+
10+
# 1928
11+
12+
Successfully leveraged serde's attributes to de and re serialise our models.
13+
Shuffled around the structure and changed some privacy settings on the code.
14+
Called our endpoint from main successfully.
15+
TODO
16+
Test standard HTTP error cases
17+
Port the peripheral error handling from the original client
18+
19+
# 1950

Ticket/20240810.md

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# 0900
2+
3+
Discovered Url::parse_with_params to avoid the format macro. Reread the TikTok client for the plan for my errors.
4+
No need to explicitly state the headers. Using the .json method sets it anyway.

Ticket/20240811.md

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# 2100
2+
3+
Made a lot of progress on error handling and testing. I am putting more effort into the modelling of the API in
4+
Rust than I probably need to, but it is useful to separate the concerns of the API representation and what Arwen
5+
wants to do with it, which will come in the service.
6+
7+
There are four tests which now pass.
8+
9+
There are still lots of warnings, but the Perspective-facing contract is functional.
10+
The next step is to build the Arwen-facing API, emulating the typescript function and its specifics.
11+
12+
# 2350

Ticket/20240813.md

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# 1000
2+
3+
Began on the service function implementation. There are two options for making the options more
4+
ergonomic to use for a developer. The first is the way I would default to if I were in another
5+
language. The second is to use the builder pattern for the request, which would have in its
6+
methods ways to control all the features of the API in a compact way. I can also introduce a default
7+
implementation which can facilitate the convenience function.
8+
9+
# 1400
10+
11+
Committed my first works on the service function. Close to completion.

Ticket/README.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
In the absence of a work tracking system, I will add comments and attachments in this directory that I would otherwise put elsewhere.
2+
3+
Additionally, since this is a coding challenge for just me, I will be pushing changes directly to main.

rustfmt.toml

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
max_width = 150
2+
use_small_heuristics = "Default"
3+
wrap_comments = true

src/client/mod.rs

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/// The Perspective client for version v1alpha1 of their API.
2+
pub(crate) mod v1alpha1;
3+
pub use v1alpha1::models::error::AnalyzeCommentError;
4+
pub use v1alpha1::models::error::AnalyzeCommentErrorResponse;
5+
pub use v1alpha1::models::error::MAX_LENGTH;
6+
pub use v1alpha1::models::request::AnalyzeCommentRequest;
7+
pub use v1alpha1::models::response::AnalyzeCommentResponse;
8+
pub use v1alpha1::PerspectiveClient;
9+
10+
// This client is versioned by API. When a new version of the Perspective API is released, a new module should be created here.

src/client/v1alpha1/analyze.rs

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
use reqwest::StatusCode;
2+
use tracing::debug;
3+
use url::Url;
4+
5+
use crate::client::AnalyzeCommentError;
6+
use crate::client::AnalyzeCommentErrorResponse;
7+
use crate::client::AnalyzeCommentRequest;
8+
use crate::client::AnalyzeCommentResponse;
9+
use crate::client::PerspectiveClient;
10+
11+
use super::BASE_URL;
12+
// Tests for the interaction with the API are in the `tests` directory.
13+
impl PerspectiveClient {
14+
/// This function encapsulates the interaction with the Perspective API, taking a request object and returning a success response or a custom error.
15+
pub async fn analyze(&self, request: AnalyzeCommentRequest) -> Result<AnalyzeCommentResponse, AnalyzeCommentError> {
16+
let url = Url::parse_with_params(BASE_URL, [("key", &self.api_key)]).expect("no api_keys can fail this function and the base url is const");
17+
debug!("URL: {}", &url);
18+
19+
let response = self.client.post(url).json(&request).send().await?;
20+
21+
match response.status() {
22+
StatusCode::OK => {
23+
let body = &response.text().await?;
24+
debug!("Response: {}", &body);
25+
Ok(serde_json::from_str(body)?)
26+
}
27+
StatusCode::FORBIDDEN => Err(AnalyzeCommentError::NoApiKey),
28+
_ => {
29+
let body = &response.text().await?;
30+
debug!("Response: {}", &body);
31+
Err(serde_json::from_str::<AnalyzeCommentErrorResponse>(body)?)?
32+
}
33+
}
34+
}
35+
}

src/client/v1alpha1/mod.rs

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
pub(crate) mod analyze;
2+
pub(crate) mod models;
3+
4+
// client constants
5+
pub(crate) const BASE_URL: &str = "https://commentanalyzer.googleapis.com/v1alpha1/comments:analyze";
6+
7+
use reqwest::Client;
8+
9+
/// A wrapper for a `reqwest` client and your API key, and has the API entry points.
10+
pub struct PerspectiveClient {
11+
pub client: Client,
12+
pub api_key: String,
13+
}
14+
15+
impl PerspectiveClient {
16+
pub fn new(api_key: &str) -> Self {
17+
Self { client: Client::new(), api_key: api_key.to_owned() }
18+
}
19+
}

0 commit comments

Comments
 (0)