Skip to content

Commit

Permalink
add post method to create new question (#1451)
Browse files Browse the repository at this point in the history
## Problem

http interface currently does not have a way to create question with it.
But it is needed to e.g. ask questions from autoyast converter script.


## Solution

Add it and together also remove password from question as it should not
be preset.
  • Loading branch information
jreidinger authored Jul 10, 2024
2 parents 719d8a5 + ef9a2bb commit 8fb914b
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 20 deletions.
94 changes: 74 additions & 20 deletions rust/agama-server/src/questions/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@
use crate::{error::Error, web::Event};
use agama_lib::{
error::ServiceError,
proxies::{GenericQuestionProxy, QuestionWithPasswordProxy},
proxies::{GenericQuestionProxy, QuestionWithPasswordProxy, Questions1Proxy},
};
use anyhow::Context;
use axum::{
extract::{Path, State},
routing::{get, put},
Json, Router,
};
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, pin::Pin};
use tokio_stream::{Stream, StreamExt};
Expand All @@ -25,11 +26,12 @@ use zbus::{
zvariant::{ObjectPath, OwnedObjectPath},
};

// TODO: move to lib
// TODO: move to lib or maybe not and just have in lib client for http API?
#[derive(Clone)]
struct QuestionsClient<'a> {
connection: zbus::Connection,
objects_proxy: ObjectManagerProxy<'a>,
questions_proxy: Questions1Proxy<'a>,
}

impl<'a> QuestionsClient<'a> {
Expand All @@ -38,6 +40,7 @@ impl<'a> QuestionsClient<'a> {
OwnedObjectPath::from(ObjectPath::try_from("/org/opensuse/Agama1/Questions")?);
Ok(Self {
connection: dbus.clone(),
questions_proxy: Questions1Proxy::new(&dbus).await?,
objects_proxy: ObjectManagerProxy::builder(&dbus)
.path(question_path)?
.destination("org.opensuse.Agama1")?
Expand All @@ -46,6 +49,47 @@ impl<'a> QuestionsClient<'a> {
})
}

pub async fn create_question(&self, question: Question) -> Result<Question, ServiceError> {
// TODO: ugly API is caused by dbus method to create question. It can be changed in future as DBus is internal only API
let generic = &question.generic;
let options: Vec<&str> = generic.options.iter().map(String::as_ref).collect();
let data: HashMap<&str, &str> = generic
.data
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();
let path = if question.with_password.is_some() {
self.questions_proxy
.new_with_password(
&generic.class,
&generic.text,
&options,
&generic.default_option,
data,
)
.await?
} else {
self.questions_proxy
.new_question(
&generic.class,
&generic.text,
&options,
&generic.default_option,
data,
)
.await?
};
let mut res = question.clone();
// we are sure that regexp is correct, so use unwrap
let id_matcher = Regex::new(r"/(?<id>\d+)$").unwrap();
let Some(id_cap) = id_matcher.captures(path.as_str()) else {
let msg = format!("Failed to get ID for new question: {}", path.as_str()).to_string();
return Err(ServiceError::UnsuccessfulAction(msg));
}; // TODO: better error if path does not contain id
res.generic.id = id_cap["id"].parse::<u32>().unwrap();
Ok(question)
}
pub async fn questions(&self) -> Result<Vec<Question>, ServiceError> {
let objects = self
.objects_proxy
Expand All @@ -59,15 +103,15 @@ impl<'a> QuestionsClient<'a> {
);
for (path, interfaces_hash) in objects.iter() {
if interfaces_hash.contains_key(&password_interface) {
result.push(self.create_question_with_password(path).await?)
result.push(self.build_question_with_password(path).await?)
} else {
result.push(self.create_generic_question(path).await?)
result.push(self.build_generic_question(path).await?)
}
}
Ok(result)
}

async fn create_generic_question(
async fn build_generic_question(
&self,
path: &OwnedObjectPath,
) -> Result<Question, ServiceError> {
Expand All @@ -91,19 +135,12 @@ impl<'a> QuestionsClient<'a> {
Ok(result)
}

async fn create_question_with_password(
async fn build_question_with_password(
&self,
path: &OwnedObjectPath,
) -> Result<Question, ServiceError> {
let dbus_question = QuestionWithPasswordProxy::builder(&self.connection)
.path(path)?
.cache_properties(zbus::CacheProperties::No)
.build()
.await?;
let mut result = self.create_generic_question(path).await?;
result.with_password = Some(QuestionWithPassword {
password: dbus_question.password().await?,
});
let mut result = self.build_generic_question(path).await?;
result.with_password = Some(QuestionWithPassword {});

Ok(result)
}
Expand Down Expand Up @@ -171,12 +208,13 @@ pub struct GenericQuestion {
/// is that it is not composition as used here, but more like
/// child of generic question and contain reference to Base.
/// Here for web API we want to have in json that separation that would
/// allow to compose any possible future specialization of question
/// allow to compose any possible future specialization of question.
/// Also note that question is empty as QuestionWithPassword does not
/// provide more details for question, but require additional answer.
/// Can be potentionally extended in future e.g. with list of allowed characters?
#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct QuestionWithPassword {
password: String,
}
pub struct QuestionWithPassword {}

#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
Expand Down Expand Up @@ -204,7 +242,7 @@ pub async fn questions_service(dbus: zbus::Connection) -> Result<Router, Service
let questions = QuestionsClient::new(dbus.clone()).await?;
let state = QuestionsState { questions };
let router = Router::new()
.route("/", get(list_questions))
.route("/", get(list_questions).post(create_question))
.route("/:id/answer", put(answer))
.with_state(state);
Ok(router)
Expand Down Expand Up @@ -266,3 +304,19 @@ async fn answer(
state.questions.answer(question_id, answer).await?;
Ok(())
}

/// Create new question.
///
/// * `state`: service state.
/// * `question`: struct with question where id of question is ignored and will be assigned
#[utoipa::path(post, path = "/questions", responses(
(status = 200, description = "answer question"),
(status = 400, description = "The D-Bus service could not perform the action")
))]
async fn create_question(
State(state): State<QuestionsState<'_>>,
Json(question): Json<Question>,
) -> Result<Json<Question>, Error> {
let res = state.questions.create_question(question).await?;
Ok(Json(res))
}
6 changes: 6 additions & 0 deletions rust/package/agama.changes
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
-------------------------------------------------------------------
Wed Jul 10 10:01:18 UTC 2024 - Josef Reidinger <jreidinger@suse.com>

- Add to HTTP API method POST for question to ask new question
(gh#openSUSE/agama#1451)

-------------------------------------------------------------------
Fri Jul 5 13:17:17 UTC 2024 - José Iván López González <jlopez@suse.com>

Expand Down

0 comments on commit 8fb914b

Please sign in to comment.