Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spike for constraints #27

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open

Spike for constraints #27

wants to merge 12 commits into from

Conversation

sighphyre
Copy link
Member

This implements extended support for constraint operators, reference implementation on the node SDK is here: https://github.com/Unleash/unleash-client-node/pull/289/files

Note that the client specs have not been rolled forward to cover these cases, since the specs also include cases for custom stickiness, which isn't as of yet implemented in this SDK, this was covered by local testing against the spec

src/strategy.rs Outdated
expression: ConstraintExpression,
inverted: Option<bool>,
getter: F,
) -> Evaluate
where
F: Fn(&Context) -> Option<&String> + Clone + Sync + Send + 'static,
{
match &expression {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This match is getting a little bit long. I'm struggling to break it out into more granular functions due to rust marking closures as unique types. I'm very open to suggestions if someone feels the same as me and has clever ideas here

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://docs.rs/enum_dispatch/latest/enum_dispatch/ is probably the thing to investigate.

We'd want to refactor the api type as well to have layers:

#[serde(flatten)]
enum ConstraintExpression{
   NotIn(NotInExpression),
   ...

Or something like that.

I suggest not trying that though, until we've removed the code duplication below.

Copy link
Collaborator

@rbtcollins rbtcollins left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the tl;dr is:

  • the test suite is too long and not interestingly-different: lets bring in proptest
  • there is too much code duplication in the new code paths

I've made some concrete suggestions on how to address these issues inline in the review.

It will be very nice to have this feature!

#[serde(flatten)]
pub expression: ConstraintExpression,
}

#[derive(Clone, Serialize, Deserialize, Debug)]
#[serde(tag = "operator", content = "values")]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a shame this needs to change: the enums are much less ergonomic now :( - single attribute structs :(.

I wonder if struct flattening + an internally tagged representation could work: a family of related enums:

  • one with values
  • one with value

@@ -40,4 +41,10 @@ pub struct Context {
pub app_name: String,
#[serde(default)]
pub environment: String,
#[serde(default = "current_time", rename = "currentTime")]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should test #[serde(rename_all = "camelCase")] at some point, might be sufficient.

@@ -40,4 +41,10 @@ pub struct Context {
pub app_name: String,
#[serde(default)]
pub environment: String,
#[serde(default = "current_time", rename = "currentTime")]
pub current_time: String,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't be a string.

IIRC correctly chrono doesn't have a sensible default, so I suggest:
a Chrono tuple newtype with serde derived impls, a std::default::Default implementation for that, then inclusion here, which as the chrono::serde default is rfc3339 should just work.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact, I'm not sure whether we should be capturing the current time at all. For the proxy use case, clients should not be permitted to time-travel; for the non-proxy use case access to the current time is probably cheap enough, since time access is such a well known thing.

We could compile rules that relate to get compiled into a unix time expression for even faster evaluation, if scaling the proxy is required - though I suspect only unleash themselves would hit that scale.

fn _compile_constraint_string<F>(expression: ConstraintExpression, getter: F) -> Evaluate
fn _compile_constraint_string<F>(
expression: ConstraintExpression,
inverted: Option<bool>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than inverted being Option, I think it wants to be a type parameter: we want to have unchanging code in this function whose types change depending on the parameter.

fn _compile_constraint_string<F, B>(expression: ConstraintExpression, getter: F, boxer:B) -> Evaluate where
F: ...,
B: Fn(Fn(&Context) -> bool) -> Evaluate
{
 ...
        ConstraintExpression::In { values } => {
            let as_set: HashSet<String> = values.iter().cloned().collect();
            boxer(move |context: &Context| {
                getter(context).map(|v| as_set.contains(v)).unwrap_or(false)
            })
        }
        ....

May need a little code golf to make it work, but I think this would be a nice place to get to.

src/strategy.rs Outdated
expression: ConstraintExpression,
inverted: Option<bool>,
getter: F,
) -> Evaluate
where
F: Fn(&Context) -> Option<&String> + Clone + Sync + Send + 'static,
{
match &expression {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://docs.rs/enum_dispatch/latest/enum_dispatch/ is probably the thing to investigate.

We'd want to refactor the api type as well to have layers:

#[serde(flatten)]
enum ConstraintExpression{
   NotIn(NotInExpression),
   ...

Or something like that.

I suggest not trying that though, until we've removed the code duplication below.

src/strategy.rs Outdated
})
}
}
ConstraintExpression::StrEndsWith {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

theres lots of duplication with StrStartsWith: I think a helper function generic over the predicate would be super useful here.

src/strategy.rs Outdated
})
}
}
ConstraintExpression::StrContains {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

src/strategy.rs Outdated
})
}
}
ConstraintExpression::NumEq { value } => match value.parse::<f64>() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and this family of contraints should also be extractable into composable helpers

src/strategy.rs Outdated
@@ -332,16 +561,17 @@ where
}
false
})
.unwrap_or(false)
.unwrap_or(false);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An alternative idea is a helper trait on Bool adding .inverted(inverted) - that will allow

.unwrap_or(false).inverted(inverted)

..Default::default()
};
// starts with matches string start
assert!(super::constrain(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we probably want to bring proptest in at this point

@chriswk
Copy link
Member

chriswk commented Mar 14, 2022

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants