dmntk_server/
server.rs

1use crate::data::ApplicationData;
2use actix_web::{post, web, App, HttpResponse, HttpServer};
3use dmntk_common::{ColorPalette, Jsonify};
4use dmntk_feel::FeelScope;
5use dmntk_workspace::Workspaces;
6use std::borrow::Borrow;
7use std::net::IpAddr;
8use std::path::{Path, PathBuf};
9use std::str::FromStr;
10use std::sync::Arc;
11use std::{env, io};
12
13const DMNTK_DEFAULT_PORT: u16 = 22022;
14const DMNTK_DEFAULT_HOST: &str = "0.0.0.0";
15const DMNTK_HOST_VARIABLE: &str = "DMNTK_HOST";
16const DMNTK_PORT_VARIABLE: &str = "DMNTK_PORT";
17const DMNTK_DIR_VARIABLE: &str = "DMNTK_DIR";
18const CONTENT_TYPE: &str = "application/json";
19
20/// Handler for evaluating invocable identified
21/// by unique name in namespace represented by RDNN.
22#[post("/evaluate/{path:.*}")]
23async fn evaluate(path: web::Path<String>, request_body: String, data: web::Data<ApplicationData>) -> HttpResponse {
24  let workspace: &Workspaces = data.workspaces.borrow();
25  match dmntk_evaluator::evaluate_context(&FeelScope::default(), &request_body).and_then(|input_data| workspace.evaluate(&path, &input_data)) {
26    Ok(value) => HttpResponse::Ok().content_type(CONTENT_TYPE).body(format!(r#"{{"data":{}}}"#, value.jsonify())),
27    Err(reason) => HttpResponse::Ok().content_type(CONTENT_TYPE).body(format!(r#"{{"errors":[{{"detail":"{reason}"}}]}}"#)),
28  }
29}
30
31/// Handler for 404 errors.
32async fn not_found() -> HttpResponse {
33  HttpResponse::NotFound().content_type(CONTENT_TYPE).body(r#"{"errors":[{"detail":"endpoint not found"}]}"#)
34}
35
36#[cfg(feature = "tck")]
37fn config(cfg: &mut web::ServiceConfig) {
38  cfg.service(crate::tck::post_tck_evaluate);
39}
40
41#[cfg(not(feature = "tck"))]
42fn config(cfg: &mut web::ServiceConfig) {
43  cfg.service(evaluate);
44}
45
46/// Starts the server.
47pub async fn start_server(opt_host: Option<String>, opt_port: Option<String>, opt_dir: Option<String>, colors: ColorPalette, verbose: bool) -> io::Result<()> {
48  let application_data = web::Data::new(ApplicationData {
49    workspaces: Arc::new(Workspaces::new(&get_root_dir(opt_dir), colors.clone(), verbose)),
50  });
51  let address = get_server_address(opt_host, opt_port);
52  println!("{1}dmntk{0} {2}{address}{0}", colors.reset(), colors.blue(), colors.yellow());
53  HttpServer::new(move || {
54    App::new()
55      .app_data(application_data.clone())
56      .app_data(web::PayloadConfig::new(4 * 1024 * 1024))
57      .configure(config)
58      .default_service(web::route().to(not_found))
59  })
60  .bind(address)?
61  .run()
62  .await
63}
64
65/// Returns the host address and the port number, the server will start to listen on.
66///
67/// The default host and port are defined by `DMNTK_DEFAULT_HOST` and `DMNTK_DEFAULT_PORT` constants.
68/// When other values are given as parameters to this function, these will be the actual host and port.
69/// Host and port may be also controlled using environment variables:
70/// - `DMNTK_HOST` for the host name,
71/// - `DMNTK_PORT` for the port name.
72///
73/// Priority (from highest to lowest):
74/// - `opt_host` an `opt_port` parameters,
75/// - `DMNTK_HOST` and `DMNTK_PORT` environment variables
76/// - `DMNTK_DEFAULT_HOST` and `DMNTK_DEFAULT_PORT` constants.
77///
78fn get_server_address(opt_host: Option<String>, opt_port: Option<String>) -> String {
79  // resolve IP address
80  let mut host = DMNTK_DEFAULT_HOST.to_string();
81  if let Ok(host_ip_address) = env::var(DMNTK_HOST_VARIABLE) {
82    if is_valid_ip_address(&host_ip_address) {
83      host = host_ip_address;
84    } else {
85      eprintln!("invalid host address specified in environment variable {}: {}", DMNTK_HOST_VARIABLE, host_ip_address);
86    }
87  }
88  if let Some(host_ip_address) = opt_host {
89    if is_valid_ip_address(&host_ip_address) {
90      host = host_ip_address;
91    } else {
92      eprintln!("invalid host address given as command option: {}", host_ip_address);
93    }
94  }
95  // resolve IP port
96  let mut port: u16 = DMNTK_DEFAULT_PORT;
97  if let Ok(p_str) = env::var(DMNTK_PORT_VARIABLE) {
98    if let Ok(p) = u16::from_str(&p_str) {
99      port = p;
100    } else {
101      eprintln!("invalid port number specified in environment variable {}: {}", DMNTK_PORT_VARIABLE, p_str);
102    }
103  }
104  if let Some(p_str) = opt_port {
105    if let Ok(p) = u16::from_str(&p_str) {
106      port = p;
107    } else {
108      eprintln!("invalid port number specified as command option: {}", p_str);
109    }
110  }
111  let server_address = format!("{host}:{port}");
112  server_address
113}
114
115/// Checks if the specified IP address is correct.
116///
117/// This function may provide more detailed checks
118/// when the [Ipv4Addr](std::net::Ipv4Addr)
119/// and [Ipv6Addr](std::net::Ipv6Addr) stabilize.
120fn is_valid_ip_address(ip: &str) -> bool {
121  ip == "localhost" || ip.parse::<IpAddr>().is_ok()
122}
123
124/// Returns the root directory for loading workspaces.
125fn get_root_dir(opt_dir: Option<String>) -> PathBuf {
126  let current_dir_path = env::current_dir().expect("failed to retrieve current directory");
127  if let Ok(s) = env::var(DMNTK_DIR_VARIABLE) {
128    let dir_path = Path::new(&s);
129    if dir_path.exists() && dir_path.is_dir() {
130      return dir_path.into();
131    } else {
132      eprintln!("invalid directory specified in environment variable {}: {}", DMNTK_DIR_VARIABLE, s);
133    }
134  }
135  if let Some(s) = opt_dir {
136    let dir_path = Path::new(&s);
137    if dir_path.exists() && dir_path.is_dir() {
138      return dir_path.into();
139    } else {
140      eprintln!("invalid directory specified as command option: {}", s);
141    }
142  }
143  current_dir_path
144}