Skip to main content

pact_plugin_driver/
utils.rs

1//! Utils for dealing with protobufs structs
2
3use std::collections::{BTreeMap, HashMap};
4use anyhow::bail;
5use itertools::Itertools;
6use os_info::Type;
7
8use prost_types::{ListValue, Struct};
9use prost_types::value::Kind;
10use semver::{Version, VersionReq};
11use serde_json::{json, Value};
12use tracing::debug;
13
14/// Converts a map of key -> JSON to a prost Struct
15pub fn to_proto_struct(values: &HashMap<String, Value>) -> Struct {
16  Struct {
17    fields: values.iter().map(|(k, v)| (k.clone(), to_proto_value(v))).collect()
18  }
19}
20
21/// Converts a JSON value to a prost struct
22pub fn to_proto_value(val: &Value) -> prost_types::Value {
23  match val {
24    Value::Null => prost_types::Value { kind: Some(prost_types::value::Kind::NullValue(0)) },
25    Value::Bool(b) => prost_types::Value { kind: Some(prost_types::value::Kind::BoolValue(*b)) },
26    Value::Number(n) => if let Some(n) = n.as_u64() {
27      prost_types::Value { kind: Some(prost_types::value::Kind::NumberValue(n as f64)) }
28    } else if let Some(n) = n.as_i64() {
29      prost_types::Value { kind: Some(prost_types::value::Kind::NumberValue(n as f64)) }
30    } else {
31      prost_types::Value { kind: Some(prost_types::value::Kind::NumberValue(n.as_f64().unwrap_or_default())) }
32    }
33    Value::String(s) => prost_types::Value { kind: Some(prost_types::value::Kind::StringValue(s.clone())) },
34    Value::Array(a) => prost_types::Value { kind: Some(prost_types::value::Kind::ListValue(ListValue {
35      values: a.iter().map(|v| to_proto_value(v)).collect()
36    }))},
37    Value::Object(o) => prost_types::Value { kind: Some(prost_types::value::Kind::StructValue(Struct {
38      fields: o.iter().map(|(k, v)| (k.clone(), to_proto_value(v))).collect()
39    }))}
40  }
41}
42
43/// Converts a prost struct to JSON value
44pub fn proto_struct_to_json(val: &prost_types::Struct) -> Value {
45  Value::Object(val.fields.iter()
46    .map(|(k, v)| (k.clone(), proto_value_to_json(v))).collect())
47}
48
49/// Converts a prost value to JSON value
50pub fn proto_value_to_json(val: &prost_types::Value) -> Value {
51  match &val.kind {
52    Some(kind) => match kind {
53      Kind::NullValue(_) => Value::Null,
54      Kind::NumberValue(n) => {
55        if n.fract() == 0.0 && *n >= i64::MIN as f64 && *n <= i64::MAX as f64 {
56          json!(*n as i64)
57        } else {
58          json!(n)
59        }
60      }
61      Kind::StringValue(s) => Value::String(s.clone()),
62      Kind::BoolValue(b) => Value::Bool(*b),
63      Kind::StructValue(s) => proto_struct_to_json(s),
64      Kind::ListValue(l) => Value::Array(l.values.iter()
65        .map(|v| proto_value_to_json(v)).collect())
66    }
67    None => Value::Null
68  }
69}
70
71/// Converts a prost struct to a map of key -> JSON
72pub fn proto_struct_to_map(val: &prost_types::Struct) -> BTreeMap<String, Value> {
73  val.fields.iter()
74    .sorted_by(|(k1, _), (k2, _)| Ord::cmp(k1, k2))
75    .map(|(k, v)| (k.clone(), proto_value_to_json(v)))
76    .collect()
77}
78
79/// Converts a prost struct to a hash map of key -> JSON
80pub fn proto_struct_to_hashmap(val: &prost_types::Struct) -> HashMap<String, Value> {
81  val.fields.iter()
82    .map(|(k, v)| (k.clone(), proto_value_to_json(v)))
83    .collect()
84}
85
86/// Convert a proto struct into a String
87pub fn proto_value_to_string(val: &prost_types::Value) -> Option<String> {
88  match &val.kind {
89    Some(kind) => match kind {
90      Kind::NullValue(_) => None,
91      Kind::NumberValue(n) => Some(n.to_string()),
92      Kind::StringValue(s) => Some(s.clone()),
93      Kind::BoolValue(b) => Some(b.to_string()),
94      Kind::StructValue(s) => Some(proto_struct_to_json(s).to_string()),
95      Kind::ListValue(l) => Some(Value::Array(l.values.iter()
96        .map(|v| proto_value_to_json(v)).collect()).to_string())
97    }
98    None => None
99  }
100}
101
102/// Check if the versions are compatible (differ in patch version only)
103pub fn versions_compatible(version: &str, required: &Option<String>) -> bool {
104  match required {
105    None => true,
106    Some(required) => {
107      if required == version {
108        true
109      } else if let Ok(version) = lenient_semver::parse(version) {
110        if let Ok(required) = lenient_semver::parse(required) {
111          version >= drop_pre_release(required)
112        } else {
113          if let Ok(req) = VersionReq::parse(format!(">={}", required).as_str()) {
114            req.matches(&version)
115          } else {
116            false
117          }
118        }
119      } else {
120        false
121      }
122    }
123  }
124}
125
126fn drop_pre_release(version: Version) -> Version {
127  Version::new(version.major, version.minor, version.patch)
128}
129
130pub fn optional_string<S: Into<String>>(string: S) -> Option<String> {
131  let string = string.into();
132  if string.is_empty() {
133    None
134  } else {
135    Some(string)
136  }
137}
138
139/// Returns the current running OS and architecture
140pub fn os_and_arch() -> anyhow::Result<(&'static str, &'static str)> {
141  let os_info = os_info::get();
142  debug!("Detected OS: {}", os_info);
143
144  let os = match os_info.os_type() {
145    Type::Alpine | Type::Amazon| Type::Android| Type::Arch| Type::CentOS| Type::Debian |
146    Type::EndeavourOS | Type::Fedora | Type::Gentoo | Type::Linux | Type::Manjaro | Type::Mariner |
147    Type::Mint | Type::NixOS | Type::openSUSE | Type::OracleLinux | Type::Redhat |
148    Type::RedHatEnterprise | Type::Pop | Type::Raspbian | Type::Solus | Type::SUSE |
149    Type::Ubuntu => "linux",
150    Type::Macos => "osx",
151    Type::Windows => "windows",
152    _ => bail!("{} is not a supported operating system", os_info)
153  };
154
155  Ok((os, std::env::consts::ARCH))
156}
157
158#[cfg(test)]
159mod tests {
160  use expectest::prelude::*;
161  use serde_json::json;
162
163  use super::{proto_value_to_json, versions_compatible};
164
165  #[test]
166  fn proto_value_to_json_preserves_integers() {
167    let make_num = |n: f64| prost_types::Value {
168      kind: Some(prost_types::value::Kind::NumberValue(n)),
169    };
170
171    // Whole-number floats must round-trip as JSON integers, not floats.
172    assert_eq!(proto_value_to_json(&make_num(2.0)), json!(2));
173    assert_eq!(proto_value_to_json(&make_num(0.0)), json!(0));
174    assert_eq!(proto_value_to_json(&make_num(-42.0)), json!(-42));
175
176    // Actual fractional values must stay as floats.
177    assert_eq!(proto_value_to_json(&make_num(2.5)), json!(2.5));
178    assert_eq!(proto_value_to_json(&make_num(-0.1)), json!(-0.1));
179  }
180
181  #[test]
182  fn versions_compatible_test() {
183    expect!(versions_compatible("1.0.0", &None)).to(be_true());
184    expect!(versions_compatible("1.0.0", &Some("1.0.0".to_string()))).to(be_true());
185    expect!(versions_compatible("1.0.0", &Some("1.0.1".to_string()))).to(be_false());
186    expect!(versions_compatible("1.0.4", &Some("1.0.3".to_string()))).to(be_true());
187    expect!(versions_compatible("1.1.0", &Some("1.0.3".to_string()))).to(be_true());
188    expect!(versions_compatible("2.0.1", &Some("1.1.0".to_string()))).to(be_true());
189    expect!(versions_compatible("1.0.1", &Some("2.1.0".to_string()))).to(be_false());
190    expect!(versions_compatible("0.1.0", &Some("0.0.3".to_string()))).to(be_true());
191  }
192
193  #[test]
194  fn versions_compatible_accepts_future_dev_versions() {
195    expect!(versions_compatible("0.1", &Some("0.1.0-beta.1".to_string()))).to(be_true());
196  }
197}