From fff5b0fc5fc9cb2f0ca5f3e7e3fadad9afc06580 Mon Sep 17 00:00:00 2001 From: Franco Testagrossa Date: Sat, 27 Apr 2024 00:11:25 +0200 Subject: [PATCH] Resolve #1 --- Cargo.lock | 24 ++--- Makefile | 12 ++- backend/Cargo.toml | 1 - backend/backend.did | 14 ++- backend/src/lib.rs | 237 ++++++++++++++++++-------------------------- 5 files changed, 132 insertions(+), 156 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c11dac9..89aba1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,6 +38,18 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backend" +version = "0.1.0" +dependencies = [ + "candid", + "ic-cdk", + "ic-cdk-macros", + "serde", + "serde_bytes", + "serde_json", +] + [[package]] name = "beef" version = "0.5.2" @@ -766,18 +778,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "backend" -version = "0.1.0" -dependencies = [ - "candid", - "ic-cdk", - "ic-cdk-macros", - "serde", - "serde_bytes", - "serde_json", -] - [[package]] name = "serde" version = "1.0.188" diff --git a/Makefile b/Makefile index 40c2fc2..84edd73 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,6 @@ +# Load environment variables from .env file +include .env + .PHONY: all all: build @@ -25,8 +28,13 @@ upgrade: build .PHONY: test .SILENT: test test: install - dfx canister call backend get_icp_usd_exchange \ - | grep '\[1682978460,5\.714,5\.718,5\.714,5\.714,243\.5678\]' && echo 'PASS' + # Call the backend canister to get the GitHub issue and capture the output + @echo "Calling backend canister..." + @TMP_FILE=$$(mktemp); \ + dfx canister call backend get_gh_issue '("${GITHUB_TOKEN}")' > $$TMP_FILE; \ + echo "get_gh_issue response:"; \ + cat $$TMP_FILE; \ + rm -f $$TMP_FILE .PHONY: clean .SILENT: clean diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 28907dc..01cb9f9 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -15,4 +15,3 @@ ic-cdk-macros = "0.6.0" serde = "1.0.152" serde_json = "1.0.93" serde_bytes = "0.11.9" - diff --git a/backend/backend.did b/backend/backend.did index fe07c38..6b2d058 100644 --- a/backend/backend.did +++ b/backend/backend.did @@ -1,3 +1,13 @@ +type issue = record { + state: opt text; + login: opt text; + id: opt text; + milestone_state: opt text; + closed_at: opt text; +}; + +type gh_token = text; + service : { - "get_icp_usd_exchange": () -> (text); -} + "get_gh_issue": (gh_token) -> (issue); +} \ No newline at end of file diff --git a/backend/src/lib.rs b/backend/src/lib.rs index f1b6519..d3d79f2 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -1,170 +1,129 @@ -//1. IMPORT IC MANAGEMENT CANISTER -//This includes all methods and types needed +use serde::{Deserialize, Serialize}; use ic_cdk::api::management_canister::http_request::{ - http_request, CanisterHttpRequestArgument, HttpHeader, HttpMethod, HttpResponse, TransformArgs, - TransformContext, + http_request, CanisterHttpRequestArgument, HttpHeader, HttpMethod, HttpResponse }; - -use ic_cdk_macros::{self, query, update}; -use serde::{Serialize, Deserialize}; -use serde_json::{self, Value}; - -// This struct is legacy code and is not really used in the code. -#[derive(Serialize, Deserialize)] -struct Context { - bucket_start_time_index: usize, - closing_price_index: usize, +use serde_json::Value; +use candid::CandidType; + +// Define the IssueResponse struct to represent the transformed response +#[derive(Debug, Serialize, Deserialize, CandidType)] +struct IssueResponse { + state: Option, + login: Option, + id: Option, + milestone_state: Option, + closed_at: Option, } -//Update method using the HTTPS outcalls feature #[ic_cdk::update] -async fn get_icp_usd_exchange() -> String { - //2. SETUP ARGUMENTS FOR HTTP GET request - - // 2.1 Setup the URL and its query parameters - type Timestamp = u64; - let start_timestamp: Timestamp = 1682978460; //May 1, 2023 22:01:00 GMT - let seconds_of_time: u64 = 60; //we start with 60 seconds - let host = "api.pro.coinbase.com"; +async fn get_gh_issue(github_token: String) -> IssueResponse { + // Setup the URL and its query parameters + let owner = "input-output-hk"; + let repo = "hydra"; + let issue_nbr = 1404; + let host = "api.github.com"; let url = format!( - "https://{}/products/ICP-USD/candles?start={}&end={}&granularity={}", - host, - start_timestamp.to_string(), - start_timestamp.to_string(), - seconds_of_time.to_string() + "https://{}/repos/{}/{}/issues/{}", + host, owner, repo, issue_nbr ); - // 2.2 prepare headers for the system http_request call - //Note that `HttpHeader` is declared in line 4 + // Prepare headers for the system http_request call let request_headers = vec![ HttpHeader { - name: "Host".to_string(), - value: format!("{host}:443"), + name: "Authorization".to_string(), + value: format!("Bearer {}", github_token), + }, + HttpHeader { + name: "Accept".to_string(), + value: "application/vnd.github+json".to_string(), }, HttpHeader { - name: "User-Agent".to_string(), - value: "exchange_rate_canister".to_string(), + name: "X-GitHub-Api-Version".to_string(), + value: "2022-11-28".to_string(), }, ]; - - // This struct is legacy code and is not really used in the code. Need to be removed in the future - // The "TransformContext" function does need a CONTEXT parameter, but this implementation is not necessary - // the TransformContext(transform, context) below accepts this "context", but it does nothing with it in this implementation. - // bucket_start_time_index and closing_price_index are meaninglesss - let context = Context { - bucket_start_time_index: 0, - closing_price_index: 4, - }; - - //note "CanisterHttpRequestArgument" and "HttpMethod" are declared in line 4 + // Create the request argument let request = CanisterHttpRequestArgument { url: url.to_string(), method: HttpMethod::GET, - body: None, //optional for request - max_response_bytes: None, //optional for request - // transform: None, //optional for request - transform: Some(TransformContext::new(transform, serde_json::to_vec(&context).unwrap())), + body: None, + max_response_bytes: None, + transform: None, // We'll handle transformation separately headers: request_headers, }; - //3. MAKE HTTPS REQUEST AND WAIT FOR RESPONSE - - //Note: in Rust, `http_request()` already sends the cycles needed - //so no need for explicit Cycles.add() as in Motoko + // Make the HTTP request and wait for the response match http_request(request).await { - //4. DECODE AND RETURN THE RESPONSE - - //See:https://docs.rs/ic-cdk/latest/ic_cdk/api/management_canister/http_request/struct.HttpResponse.html Ok((response,)) => { - //if successful, `HttpResponse` has this structure: - // pub struct HttpResponse { - // pub status: Nat, - // pub headers: Vec, - // pub body: Vec, - // } - - //We need to decode that Vec that is the body into readable text. - //To do this, we: - // 1. Call `String::from_utf8()` on response.body - // 3. We use a switch to explicitly call out both cases of decoding the Blob into ?Text - let str_body = String::from_utf8(response.body) - .expect("Transformed response is not UTF-8 encoded."); - - //The API response will looks like this: - - // ("[[1682978460,5.714,5.718,5.714,5.714,243.5678]]") - - //Which can be formatted as this - // [ - // [ - // 1682978460, <-- start/timestamp - // 5.714, <-- low - // 5.718, <-- high - // 5.714, <-- open - // 5.714, <-- close - // 243.5678 <-- volume - // ], - // ] + // Parse the response body using the transform function + let transformed_response = transform_response(response.clone()); + // Print the transformed response for debugging + println!("Transformed response: {:?}", transformed_response); - //Return the body as a string and end the method - str_body + // Return the transformed response + transformed_response } - Err((r, m)) => { - let message = - format!("The http_request resulted into error. RejectionCode: {r:?}, Error: {m}"); - - //Return the error as a string and end the method - message + Err((rejection_code, message)) => { + panic!( + "The http_request resulted in an error. RejectionCode: {:?}, Error: {}", + rejection_code, message + ); } } } - -// Strips all data that is not needed from the original response. -#[query] -fn transform(raw: TransformArgs) -> HttpResponse { - - let headers = vec![ - HttpHeader { - name: "Content-Security-Policy".to_string(), - value: "default-src 'self'".to_string(), - }, - HttpHeader { - name: "Referrer-Policy".to_string(), - value: "strict-origin".to_string(), - }, - HttpHeader { - name: "Permissions-Policy".to_string(), - value: "geolocation=(self)".to_string(), - }, - HttpHeader { - name: "Strict-Transport-Security".to_string(), - value: "max-age=63072000".to_string(), - }, - HttpHeader { - name: "X-Frame-Options".to_string(), - value: "DENY".to_string(), - }, - HttpHeader { - name: "X-Content-Type-Options".to_string(), - value: "nosniff".to_string(), - }, - ]; - - let mut res = HttpResponse { - status: raw.response.status.clone(), - body: raw.response.body.clone(), - headers, - ..Default::default() - }; - - if res.status == 200 { - - res.body = raw.response.body; - } else { - ic_cdk::api::print(format!("Received an error from coinbase: err = {:?}", raw)); - } - res +// Define a function to transform the response body +fn transform_response( + raw_response: HttpResponse, +) -> IssueResponse { + // Deserialize the raw response body into a serde_json::Value + let parsed_response: Value = serde_json::from_slice(&raw_response.body) + .unwrap_or_else(|e| panic!("Failed to parse JSON response: {}", e)); + + // Print the parsed response for debugging + println!("Parsed response: {:?}", parsed_response); + + // Extract only the desired fields from the parsed response + let transformed_response = parsed_response + .as_object() + .and_then(|obj| { + // Extract fields from the object and construct a new object with only the desired fields + let state = obj + .get("state") + .map(|value| value.as_str().map(|s| s.to_string())) + .flatten(); + let login = obj + .get("closed_by") + .and_then(|closed_by| closed_by.get("login")) + .and_then(|value| value.as_str().map(|s| s.to_string())); + let id = obj + .get("closed_by") + .and_then(|closed_by| closed_by.get("id")) + .and_then(|value| value.as_str().map(|s| s.to_string())); + let milestone_state = obj + .get("milestone") + .and_then(|milestone| milestone.get("state")) + .and_then(|value| value.as_str().map(|s| s.to_string())); + let closed_at = obj + .get("milestone") + .and_then(|milestone| milestone.get("closed_at")) + .and_then(|value| value.as_str().map(|s| s.to_string())); + + // Construct the transformed response object + Some(IssueResponse { + state, + login, + id, + milestone_state, + closed_at, + }) + }) + .unwrap_or_else(|| panic!("Failed to extract fields from parsed response")); + + // Print the transformed response for debugging + println!("Transformed response: {:?}", transformed_response); + + transformed_response }