Skip to content

Commit

Permalink
feat: return collections as arrays, generic primary identifiers (#135)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: return collection as an array of objects instead of a map, rename primary identifier

The response types for the following endpoints have been changed from a map of objects to an array of objects:

- `/v0/holder/credentials`
- `/v0/holder/offers`
- `/v0/holder/presentations`
- `/v0/connections`
- `/v0/services`
- `/v0/credentials`
- `/v0/offers`

### Before
```
{
  {
    "credential_id": "bf766ebe-0a52-4531-ac24-88af1b854b55"
    ...
  }
}
```

### After
```
[
  {
    "id": "bf766ebe-0a52-4531-ac24-88af1b854b55"
    ...
  }
]
```

### Migration guide
- Parse responses as lists instead of objects
- Use the generic `id` property instead of having to search the response for the identifier
  • Loading branch information
nanderstabel authored Dec 3, 2024
1 parent a019dea commit e225131
Show file tree
Hide file tree
Showing 33 changed files with 317 additions and 166 deletions.
111 changes: 73 additions & 38 deletions agent_api_rest/postman/ssi-agent.postman_collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -800,13 +800,18 @@
"exec": [
"const jsonData = JSON.parse(responseBody);",
"",
"if (jsonData && typeof jsonData === 'object') {",
" const receivedOfferId = Object.keys(jsonData)[0];",
"if (Array.isArray(jsonData) && jsonData.length > 0) {",
" const lastItem = jsonData[jsonData.length - 1];",
"",
" if (lastItem && typeof lastItem === 'object') {",
" const receivedOfferId = lastItem.id;",
"",
" if (receivedOfferId) {",
" pm.collectionVariables.set(\"RECEIVED_OFFER_ID\", receivedOfferId);",
" if (receivedOfferId) {",
" pm.collectionVariables.set(\"RECEIVED_OFFER_ID\", receivedOfferId);",
" }",
" }",
"}"
"}",
""
],
"type": "text/javascript",
"packages": {}
Expand Down Expand Up @@ -911,13 +916,18 @@
"exec": [
"const jsonData = JSON.parse(responseBody);",
"",
"if (jsonData && typeof jsonData === 'object') {",
" const holderCredentialId = Object.keys(jsonData)[0];",
"if (Array.isArray(jsonData) && jsonData.length > 0) {",
" const lastItem = jsonData[jsonData.length - 1];",
"",
" if (lastItem && typeof lastItem === 'object') {",
" const holderCredentialId = lastItem.id;",
"",
" if (holderCredentialId) {",
" pm.collectionVariables.set(\"HOLDER_CREDENTIAL_ID\", holderCredentialId);",
" if (holderCredentialId) {",
" pm.collectionVariables.set(\"HOLDER_CREDENTIAL_ID\", holderCredentialId);",
" }",
" }",
"}"
"}",
""
],
"type": "text/javascript",
"packages": {}
Expand Down Expand Up @@ -977,13 +987,18 @@
"exec": [
"const jsonData = JSON.parse(responseBody);",
"",
"if (jsonData && typeof jsonData === 'object') {",
" const presentationId = Object.keys(jsonData)[0];",
"if (Array.isArray(jsonData) && jsonData.length > 0) {",
" const lastItem = jsonData[jsonData.length - 1];",
"",
" if (lastItem && typeof lastItem === 'object') {",
" const presentationId = lastItem.id;",
"",
" if (presentationId) {",
" pm.collectionVariables.set(\"PRESENTATION_ID\", presentationId);",
" if (presentationId) {",
" pm.collectionVariables.set(\"PRESENTATION_ID\", presentationId);",
" }",
" }",
"}"
"}",
""
],
"type": "text/javascript",
"packages": {}
Expand Down Expand Up @@ -1125,13 +1140,18 @@
"exec": [
"const jsonData = JSON.parse(responseBody);",
"",
"if (jsonData && typeof jsonData === 'object') {",
" const connectionId = Object.keys(jsonData)[0];",
"if (Array.isArray(jsonData) && jsonData.length > 0) {",
" const lastItem = jsonData[jsonData.length - 1];",
"",
" if (lastItem && typeof lastItem === 'object') {",
" const connectionId = lastItem.id;",
"",
" if (connectionId) {",
" pm.collectionVariables.set(\"CONNECTION_ID\", connectionId);",
" if (connectionId) {",
" pm.collectionVariables.set(\"CONNECTION_ID\", connectionId);",
" }",
" }",
"}"
"}",
""
],
"type": "text/javascript",
"packages": {}
Expand Down Expand Up @@ -1173,13 +1193,18 @@
"exec": [
"const jsonData = JSON.parse(responseBody);",
"",
"if (jsonData && typeof jsonData === 'object') {",
" const connectionId = Object.keys(jsonData)[0];",
"if (Array.isArray(jsonData) && jsonData.length > 0) {",
" const lastItem = jsonData[jsonData.length - 1];",
"",
" if (connectionId) {",
" pm.collectionVariables.set(\"CONNECTION_ID\", connectionId);",
" if (lastItem && typeof lastItem === 'object') {",
" const connectionId = lastItem.id;",
"",
" if (connectionId) {",
" pm.collectionVariables.set(\"CONNECTION_ID\", connectionId);",
" }",
" }",
"}"
"}",
""
],
"type": "text/javascript"
}
Expand Down Expand Up @@ -1216,13 +1241,18 @@
"exec": [
"const jsonData = JSON.parse(responseBody);",
"",
"if (jsonData && typeof jsonData === 'object') {",
" const connectionId = Object.keys(jsonData)[0];",
"if (Array.isArray(jsonData) && jsonData.length > 0) {",
" const lastItem = jsonData[jsonData.length - 1];",
"",
" if (lastItem && typeof lastItem === 'object') {",
" const connectionId = lastItem.id;",
"",
" if (connectionId) {",
" pm.collectionVariables.set(\"CONNECTION_ID\", connectionId);",
" if (connectionId) {",
" pm.collectionVariables.set(\"CONNECTION_ID\", connectionId);",
" }",
" }",
"}"
"}",
""
],
"type": "text/javascript",
"packages": {}
Expand Down Expand Up @@ -1270,13 +1300,18 @@
"exec": [
"const jsonData = JSON.parse(responseBody);",
"",
"if (jsonData && typeof jsonData === 'object') {",
" const connectionId = Object.keys(jsonData)[0];",
"if (Array.isArray(jsonData) && jsonData.length > 0) {",
" const lastItem = jsonData[jsonData.length - 1];",
"",
" if (lastItem && typeof lastItem === 'object') {",
" const connectionId = lastItem.id;",
"",
" if (connectionId) {",
" pm.collectionVariables.set(\"CONNECTION_ID\", connectionId);",
" if (connectionId) {",
" pm.collectionVariables.set(\"CONNECTION_ID\", connectionId);",
" }",
" }",
"}"
"}",
""
],
"type": "text/javascript",
"packages": {}
Expand Down Expand Up @@ -1407,10 +1442,10 @@
"const jsonData = JSON.parse(responseBody);",
"",
"if (Array.isArray(jsonData) && jsonData.length > 0) {",
" const firstItem = jsonData[0];",
" const lastItem = jsonData[jsonData.length - 1];",
"",
" if (firstItem && typeof firstItem === 'object') {",
" const serviceId = firstItem.service_id;",
" if (lastItem && typeof lastItem === 'object') {",
" const serviceId = lastItem.id;",
"",
" if (serviceId) {",
" pm.collectionVariables.set(\"SERVICE_ID\", serviceId);",
Expand Down
8 changes: 6 additions & 2 deletions agent_api_rest/src/holder/holder/credentials/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ use serde_json::json;
#[axum_macros::debug_handler]
pub(crate) async fn credentials(State(state): State<HolderState>) -> Response {
match query_handler("all_holder_credentials", &state.query.all_holder_credentials).await {
Ok(Some(all_credentials_view)) => (StatusCode::OK, Json(all_credentials_view)).into_response(),
Ok(None) => (StatusCode::OK, Json(json!({}))).into_response(),
Ok(Some(all_credentials_view)) => {
let all_credentials = all_credentials_view.credentials.into_values().collect::<Vec<_>>();

(StatusCode::OK, Json(all_credentials)).into_response()
}
Ok(None) => (StatusCode::OK, Json(json!([]))).into_response(),
_ => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
}
}
Expand Down
30 changes: 18 additions & 12 deletions agent_api_rest/src/holder/holder/offers/accept.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,23 @@ use axum::{
use hyper::StatusCode;

#[axum_macros::debug_handler]
pub(crate) async fn accept(State(state): State<HolderState>, Path(offer_id): Path<String>) -> Response {
pub(crate) async fn accept(State(state): State<HolderState>, Path(received_offer_id): Path<String>) -> Response {
// TODO: General note that also applies to other endpoints: currently we are using Application Layer logic in the
// REST API. This is not ideal and should be changed. The REST API should only be responsible for handling HTTP
// Requests and Responses.
// Furthermore, the Application Layer (not implemented yet) should be kept very thin as well. See: https://github.com/impierce/ssi-agent/issues/114

// Accept the Credential Offer if it exists
match query_handler(&offer_id, &state.query.received_offer).await {
match query_handler(&received_offer_id, &state.query.received_offer).await {
Ok(Some(ReceivedOfferView { .. })) => {
let command = OfferCommand::AcceptCredentialOffer {
offer_id: offer_id.clone(),
received_offer_id: received_offer_id.clone(),
};

if command_handler(&offer_id, &state.command.offer, command).await.is_err() {
if command_handler(&received_offer_id, &state.command.offer, command)
.await
.is_err()
{
// TODO: add better Error responses. This needs to be done properly in all endpoints once
// https://github.com/impierce/openid4vc/issues/78 is fixed.
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
Expand All @@ -36,42 +39,45 @@ pub(crate) async fn accept(State(state): State<HolderState>, Path(offer_id): Pat
}

let command = OfferCommand::SendCredentialRequest {
offer_id: offer_id.clone(),
received_offer_id: received_offer_id.clone(),
};

// Send the Credential Request
if command_handler(&offer_id, &state.command.offer, command).await.is_err() {
if command_handler(&received_offer_id, &state.command.offer, command)
.await
.is_err()
{
// TODO: add better Error responses. This needs to be done properly in all endpoints once
// https://github.com/impierce/openid4vc/issues/78 is fixed.
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
}

let credentials = match query_handler(&offer_id, &state.query.received_offer).await {
let credentials = match query_handler(&received_offer_id, &state.query.received_offer).await {
Ok(Some(ReceivedOfferView { credentials, .. })) => credentials,
_ => return StatusCode::INTERNAL_SERVER_ERROR.into_response(),
};

for OfferCredential {
credential_id,
holder_credential_id,
credential,
} in credentials
{
let command = CredentialCommand::AddCredential {
credential_id: credential_id.clone(),
offer_id: offer_id.clone(),
holder_credential_id: holder_credential_id.clone(),
received_offer_id: received_offer_id.clone(),
credential,
};

// Add the Credential to the state.
if command_handler(&credential_id, &state.command.credential, command)
if command_handler(&holder_credential_id, &state.command.credential, command)
.await
.is_err()
{
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
}
}

match query_handler(&offer_id, &state.query.received_offer).await {
match query_handler(&received_offer_id, &state.query.received_offer).await {
Ok(Some(received_offer_view)) => (StatusCode::OK, Json(received_offer_view)).into_response(),
Ok(None) => StatusCode::NOT_FOUND.into_response(),
_ => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
Expand Down
11 changes: 9 additions & 2 deletions agent_api_rest/src/holder/holder/offers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,15 @@ use serde_json::json;
#[axum_macros::debug_handler]
pub(crate) async fn offers(State(state): State<HolderState>) -> Response {
match query_handler("all_received_offers", &state.query.all_received_offers).await {
Ok(Some(all_offers_view)) => (StatusCode::OK, Json(all_offers_view)).into_response(),
Ok(None) => (StatusCode::OK, Json(json!({}))).into_response(),
Ok(Some(all_received_offers_view)) => {
let all_received_offers = all_received_offers_view
.received_offers
.into_values()
.collect::<Vec<_>>();

(StatusCode::OK, Json(all_received_offers)).into_response()
}
Ok(None) => (StatusCode::OK, Json(json!([]))).into_response(),
_ => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
}
}
Expand Down
9 changes: 6 additions & 3 deletions agent_api_rest/src/holder/holder/offers/reject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ use axum::{
use hyper::StatusCode;

#[axum_macros::debug_handler]
pub(crate) async fn reject(State(state): State<HolderState>, Path(offer_id): Path<String>) -> Response {
pub(crate) async fn reject(State(state): State<HolderState>, Path(received_offer_id): Path<String>) -> Response {
let command = OfferCommand::RejectCredentialOffer {
offer_id: offer_id.clone(),
received_offer_id: received_offer_id.clone(),
};

// Remove the Credential Offer from the state.
if command_handler(&offer_id, &state.command.offer, command).await.is_err() {
if command_handler(&received_offer_id, &state.command.offer, command)
.await
.is_err()
{
// TODO: add better Error responses. This needs to be done properly in all endpoints once
// https://github.com/impierce/openid4vc/issues/78 is fixed.
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
Expand Down
8 changes: 6 additions & 2 deletions agent_api_rest/src/holder/holder/presentations/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@ use tracing::info;
#[axum_macros::debug_handler]
pub(crate) async fn get_presentations(State(state): State<HolderState>) -> Response {
match query_handler("all_presentations", &state.query.all_presentations).await {
Ok(Some(all_presentations_view)) => (StatusCode::OK, Json(all_presentations_view)).into_response(),
Ok(None) => (StatusCode::OK, Json(json!({}))).into_response(),
Ok(Some(all_presentations_view)) => {
let all_presentations = all_presentations_view.presentations.into_values().collect::<Vec<_>>();

(StatusCode::OK, Json(all_presentations)).into_response()
}
Ok(None) => (StatusCode::OK, Json(json!([]))).into_response(),
_ => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
}
}
Expand Down
2 changes: 1 addition & 1 deletion agent_api_rest/src/holder/openid4vci/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ pub(crate) async fn offers_inner(state: HolderState, payload: serde_json::Value)
info!("Credential Offer: {:#?}", credential_offer);

let command = OfferCommand::ReceiveCredentialOffer {
offer_id: received_offer_id.clone(),
received_offer_id: received_offer_id.clone(),
credential_offer,
};

Expand Down
12 changes: 6 additions & 6 deletions agent_api_rest/src/identity/connections/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ use identity_core::common::Url;
use identity_did::DIDUrl;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::collections::HashMap;
use tracing::info;

#[derive(Deserialize, Serialize)]
Expand Down Expand Up @@ -97,23 +96,24 @@ pub(crate) async fn get_connections(

match query_handler("all_connections", &state.query.all_connections).await {
Ok(Some(all_connections_view)) => {
let filtered_connections: HashMap<_, _> = all_connections_view
let filtered_connections: Vec<_> = all_connections_view
.connections
.into_iter()
.filter(|(_, connection)| {
alias
.filter_map(|(_, connection)| {
(alias
.as_ref()
.map_or(true, |alias| connection.alias.as_ref() == Some(alias))
&& domain
.as_ref()
.map_or(true, |domain| connection.domain.as_ref() == Some(domain))
&& did.as_ref().map_or(true, |did| connection.dids.contains(did))
&& did.as_ref().map_or(true, |did| connection.dids.contains(did)))
.then_some(connection)
})
.collect();

(StatusCode::OK, Json(filtered_connections)).into_response()
}
Ok(None) => (StatusCode::OK, Json(json!({}))).into_response(),
Ok(None) => (StatusCode::OK, Json(json!([]))).into_response(),
_ => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
}
}
Expand Down
8 changes: 6 additions & 2 deletions agent_api_rest/src/identity/services/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@ use serde_json::json;
#[axum_macros::debug_handler]
pub(crate) async fn services(State(state): State<IdentityState>) -> Response {
match query_handler("all_services", &state.query.all_services).await {
Ok(Some(all_services_view)) => (StatusCode::OK, Json(all_services_view)).into_response(),
Ok(None) => (StatusCode::OK, Json(json!({}))).into_response(),
Ok(Some(all_services_view)) => {
let all_services = all_services_view.services.into_values().collect::<Vec<_>>();

(StatusCode::OK, Json(all_services)).into_response()
}
Ok(None) => (StatusCode::OK, Json(json!([]))).into_response(),
_ => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
}
}
Expand Down
Loading

0 comments on commit e225131

Please sign in to comment.