use std::collections::BTreeMap;
use std::fmt;
use sha2::{Digest, Sha256};
use super::{DomainError, JobName, ParameterName};
const MAX_PARAMETER_STRING_BYTES: usize = 64 * 1024;
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[non_exhaustive]
pub enum ParameterValueKind {
String,
I64,
U64,
Bool,
}
impl ParameterValueKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::String => "STRING",
Self::I64 => "I64",
Self::U64 => "U64",
Self::Bool => "BOOL",
}
}
}
impl fmt::Display for ParameterValueKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct ParameterValue(ParameterValueInner);
#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
enum ParameterValueInner {
String(String),
I64(i64),
U64(u64),
Bool(bool),
}
impl ParameterValue {
pub fn string(value: impl Into<String>) -> Result<Self, DomainError> {
let value = value.into();
if value.len() > MAX_PARAMETER_STRING_BYTES {
return Err(DomainError::ParameterStringTooLong {
max_bytes: MAX_PARAMETER_STRING_BYTES,
});
}
Ok(Self(ParameterValueInner::String(value)))
}
#[must_use]
pub const fn kind(&self) -> ParameterValueKind {
match self {
Self(ParameterValueInner::String(_)) => ParameterValueKind::String,
Self(ParameterValueInner::I64(_)) => ParameterValueKind::I64,
Self(ParameterValueInner::U64(_)) => ParameterValueKind::U64,
Self(ParameterValueInner::Bool(_)) => ParameterValueKind::Bool,
}
}
#[must_use]
pub fn as_str(&self) -> Option<&str> {
match self {
Self(ParameterValueInner::String(value)) => Some(value),
_ => None,
}
}
#[must_use]
pub const fn as_i64(&self) -> Option<i64> {
match self {
Self(ParameterValueInner::I64(value)) => Some(*value),
_ => None,
}
}
#[must_use]
pub const fn as_u64(&self) -> Option<u64> {
match self {
Self(ParameterValueInner::U64(value)) => Some(*value),
_ => None,
}
}
#[must_use]
pub const fn as_bool(&self) -> Option<bool> {
match self {
Self(ParameterValueInner::Bool(value)) => Some(*value),
_ => None,
}
}
}
impl From<i64> for ParameterValue {
fn from(value: i64) -> Self {
Self(ParameterValueInner::I64(value))
}
}
impl TryFrom<String> for ParameterValue {
type Error = DomainError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::string(value)
}
}
impl TryFrom<&str> for ParameterValue {
type Error = DomainError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::string(value)
}
}
impl From<u64> for ParameterValue {
fn from(value: u64) -> Self {
Self(ParameterValueInner::U64(value))
}
}
impl From<bool> for ParameterValue {
fn from(value: bool) -> Self {
Self(ParameterValueInner::Bool(value))
}
}
impl fmt::Debug for ParameterValue {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_tuple(match self.kind() {
ParameterValueKind::String => "String",
ParameterValueKind::I64 => "I64",
ParameterValueKind::U64 => "U64",
ParameterValueKind::Bool => "Bool",
})
.field(&Redacted)
.finish()
}
}
impl fmt::Display for ParameterValue {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("<redacted>")
}
}
struct Redacted;
impl fmt::Debug for Redacted {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("<redacted>")
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum ParameterRole {
Identifying,
NonIdentifying,
}
#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct JobParameter {
value: ParameterValue,
role: ParameterRole,
}
impl JobParameter {
#[must_use]
pub const fn new(value: ParameterValue, role: ParameterRole) -> Self {
Self { value, role }
}
#[must_use]
pub const fn value(&self) -> &ParameterValue {
&self.value
}
#[must_use]
pub const fn role(&self) -> ParameterRole {
self.role
}
#[must_use]
pub const fn is_identifying(&self) -> bool {
matches!(self.role, ParameterRole::Identifying)
}
}
impl fmt::Debug for JobParameter {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("JobParameter")
.field("kind", &self.value.kind())
.field("role", &self.role)
.field("value", &Redacted)
.finish()
}
}
#[derive(Clone, Default, Eq, PartialEq)]
pub struct JobParameters {
values: BTreeMap<ParameterName, JobParameter>,
}
impl JobParameters {
#[must_use]
pub const fn new() -> Self {
Self {
values: BTreeMap::new(),
}
}
pub fn insert(
&mut self,
name: ParameterName,
parameter: JobParameter,
) -> Result<(), DomainError> {
if self.values.contains_key(&name) {
return Err(DomainError::DuplicateParameter);
}
self.values.insert(name, parameter);
Ok(())
}
pub fn try_from_iter(
parameters: impl IntoIterator<Item = (ParameterName, JobParameter)>,
) -> Result<Self, DomainError> {
let mut result = Self::new();
for (name, parameter) in parameters {
result.insert(name, parameter)?;
}
Ok(result)
}
#[must_use]
pub fn get(&self, name: &ParameterName) -> Option<&JobParameter> {
self.values.get(name)
}
#[must_use]
pub fn iter(&self) -> impl ExactSizeIterator<Item = (&ParameterName, &JobParameter)> {
self.values.iter()
}
#[must_use]
pub fn len(&self) -> usize {
self.values.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
#[must_use]
pub fn identifying_len(&self) -> usize {
self.values
.values()
.filter(|parameter| parameter.is_identifying())
.count()
}
#[must_use]
pub fn flow_input_digest(&self) -> [u8; 32] {
let mut hash = Sha256::new();
hash.update(b"oxide-batch.flow-parameters.v1\0");
for (name, parameter) in &self.values {
hash_parameter_field(&mut hash, name.as_str().as_bytes());
hash.update([match parameter.role() {
ParameterRole::Identifying => 1,
ParameterRole::NonIdentifying => 0,
}]);
match ¶meter.value.0 {
ParameterValueInner::String(value) => {
hash.update([1]);
hash_parameter_field(&mut hash, value.as_bytes());
}
ParameterValueInner::I64(value) => {
hash.update([2]);
hash.update(value.to_be_bytes());
}
ParameterValueInner::U64(value) => {
hash.update([3]);
hash.update(value.to_be_bytes());
}
ParameterValueInner::Bool(value) => {
hash.update([4, u8::from(*value)]);
}
}
}
hash.finalize().into()
}
}
fn hash_parameter_field(hash: &mut Sha256, value: &[u8]) {
hash.update(u64::try_from(value.len()).unwrap_or(u64::MAX).to_be_bytes());
hash.update(value);
}
impl fmt::Debug for JobParameters {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("JobParameters")
.field("parameter_count", &self.len())
.field("identifying_count", &self.identifying_len())
.finish_non_exhaustive()
}
}
#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct JobInstanceKey {
job_name: JobName,
identifying_parameters: BTreeMap<ParameterName, ParameterValue>,
}
impl JobInstanceKey {
#[must_use]
pub fn new(job_name: JobName, parameters: &JobParameters) -> Self {
let identifying_parameters = parameters
.iter()
.filter(|(_, parameter)| parameter.is_identifying())
.map(|(name, parameter)| (name.clone(), parameter.value().clone()))
.collect();
Self {
job_name,
identifying_parameters,
}
}
#[must_use]
pub const fn job_name(&self) -> &JobName {
&self.job_name
}
#[must_use]
pub fn identifying_parameter_count(&self) -> usize {
self.identifying_parameters.len()
}
#[must_use]
pub fn identifying_value(&self, name: &ParameterName) -> Option<&ParameterValue> {
self.identifying_parameters.get(name)
}
#[must_use]
pub fn identifying_fields(
&self,
) -> impl ExactSizeIterator<Item = (&ParameterName, ParameterValueKind)> {
self.identifying_parameters
.iter()
.map(|(name, value)| (name, value.kind()))
}
#[must_use]
pub fn digest(&self) -> [u8; 32] {
let mut encoded = Vec::new();
encoded.push(1);
push_length_prefixed(&mut encoded, self.job_name.as_str().as_bytes());
for (name, value) in &self.identifying_parameters {
push_length_prefixed(&mut encoded, name.as_str().as_bytes());
encoded.push(parameter_tag(value.kind()));
match &value.0 {
ParameterValueInner::String(value) => {
push_length_prefixed(&mut encoded, value.as_bytes());
}
ParameterValueInner::I64(value) => {
encoded.extend_from_slice(&value.to_be_bytes());
}
ParameterValueInner::U64(value) => {
encoded.extend_from_slice(&value.to_be_bytes());
}
ParameterValueInner::Bool(value) => encoded.push(u8::from(*value)),
}
}
Sha256::digest(encoded).into()
}
}
fn push_length_prefixed(target: &mut Vec<u8>, value: &[u8]) {
let length = u32::try_from(value.len()).unwrap_or(u32::MAX);
target.extend_from_slice(&length.to_be_bytes());
target.extend_from_slice(value);
}
const fn parameter_tag(kind: ParameterValueKind) -> u8 {
match kind {
ParameterValueKind::String => 1,
ParameterValueKind::I64 => 2,
ParameterValueKind::U64 => 3,
ParameterValueKind::Bool => 4,
}
}
impl fmt::Debug for JobInstanceKey {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("JobInstanceKey")
.field("job_name", &self.job_name)
.field(
"identifying_parameter_count",
&self.identifying_parameter_count(),
)
.finish_non_exhaustive()
}
}