dsntk_server/
server.rs

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