Choice rule payload validation#189
Conversation
|
update:
|
|
wip: pre-compiling operations |
a0d0c26 to
0993b77
Compare
|
update:
This is a lot more involved but a lot more strict/stringent update:
|
0993b77 to
07a1411
Compare
|
rubocop: command line and web have different opinions on whether a regex is freezable. one complains no matter what code I use here. un-wip: this seems to work |
fdab00e to
e1bce92
Compare
|
update:
update:
|
|
Can you add specs around these new classes? wondering if would catch the invalid namespace thing I found. |
e1bce92 to
4ff6108
Compare
|
update:
I commented out every line there, and the specs fail. So every one of those classes are tested |
|
Discussion: e.g.: |
| values = COMPARE_RULE.match(key) | ||
| return [key, DATA_RULES[values[2]], !!values[3]] if values |
There was a problem hiding this comment.
I think this would read better if the match was decomposed or perhaps if named captures were used instead of using values[2], values[3], etc.
Some other quetsions
- where is the prefix used (the
(String|Numeric|Boolean|Timestamp)part)? It seems to be ignored here. - The caller does
compare_key, klass = klass_params(payload), but this method returns 3 items, so is that a bug? Where did the 3rd param go with the path, and why isn't it being used?
There was a problem hiding this comment.
the current pattern for the states/command objects to parse the payload themselves.
Here, we need to parse the string to
The old code ignored the first part, so I continued to ignore it. I can see doing type checks or conversions.
returning 2 vs 3 is probably a bug. thanks
There was a problem hiding this comment.
- yes, we ignore the type prefix part and don't currently do data type conversion. (not sure if that is needed)
- changed this code. think all set.
Maybe, but this all feels overly complicated to me personally. I don't see why each of these can't be a simple method (or even just the original code the way it was). Just curious what problem you were trying to solve. I could see creating the classes to encapsulate a |
856fce4 to
1ceb8f4
Compare
|
sorry - this is for previous commit update:
|
|
update
|
|
update:
|
|
update:
|
|
update:
|
|
update:
(that hopefully will be the last code climate issue with this code) update:
|
|
update:
just superficial changes. Code hasn't changed |
| return presence_check(context, input) if compare_key == "IsPresent" | ||
|
|
||
| lhs = variable_value(context, input) | ||
| rhs = compare_value(context, input) | ||
|
|
||
| validate!(lhs) | ||
|
|
There was a problem hiding this comment.
Before:
- fetches a value, and verifies what ever came out is not null
After:
- fetches a value and verifies it existed in the input hash
| when "IsTimestamp" then is_timestamp?(lhs) | ||
| when "IsNull" then is_null?(lhs, rhs) | ||
| when "IsNumeric" then is_numeric?(lhs, rhs) | ||
| when "IsString" then is_string?(lhs, rhs) |
There was a problem hiding this comment.
Before: "IsString" => "*" verified it is a string.
After: "IsString" => true works that way, but "IsString" => false does the opposite
(across all checks)
| def variable_value(context, input) | ||
| fetch_path("Variable", variable, context, input) | ||
| end |
There was a problem hiding this comment.
(moved over from ChoiceRule since this is data specific)
Before:
- convert
@variableto aPathat runtime and fetch value.
After: @variable is aPath`, so bad Path detected at initialization time- Path not present in input is a runtime error
- Data type of input is checked.
| def parse_compare_key | ||
| @compare_key = payload.keys.detect { |key| key.match?(TYPE_CHECK) || key.match?(OPERATION) } | ||
| parser_error!("requires a compare key") unless compare_key | ||
|
|
||
| @type, _operator, @path = OPERATION.match(compare_key)&.captures | ||
| # TYPE_CHECK doesn't match this regex, so @path = @type = nil | ||
| # @path.nil? means this the compare_value will always be a static value (true or false) | ||
| # @type.nil? means we won't type check the variable or compare value | ||
| end |
There was a problem hiding this comment.
Yea, this got more complicated.
After:
- Can detect bad compare values at initialization time.
- Can detect bad paths at initialization time.
- Store type checking information (only for operations. bad type in the type check are informational (true false) rather than errors.
| results.count < 2 ? results.first : results | ||
| case results.count | ||
| when 0 | ||
| raise Floe::PathError, "Path [#{payload}] references an invalid value" |
There was a problem hiding this comment.
This one change has big consequences.
Before: A path pointing to nothing would just treat the value as nil
After: A path pointing to nothing is a runtime error
| rescue Floe::ExecutionError => e | ||
| mark_error(context, e) |
There was a problem hiding this comment.
This was detected because tests running against a state would work, but running against a workflow would not.
Before:
- Errors needed to be caught by each
State - Errors resulted in a state that was not complete.
- Errors did not always get their way into output.
After:
- runtime errors always set the context accordingly.
| def finish(context) | ||
| mark_finished(context) | ||
| end |
There was a problem hiding this comment.
The code for finish hasn't changed, it just now resides in mark_finished.
Reasoning: If State throws an error, it almost always threw it in finish. When an error is thrown, super.finish is not called. This means no logging and FinishedTime is never set -- so the state looks like it is not finished.
Now, we catch errors thrown, and ensure super.finish is called. Easiest way to do this is to move that code into a separate method -- mark_finish.
|
WIP: piece by piece pulling out PRs from this |
|
update:
update:
|
|
Checked commit kbrock@f5db130 with ruby 3.1.5, rubocop 1.56.3, haml-lint 0.62.0, and yamllint |
|
update:
update:
un-WIP: this is working, no more dependencies. want to either merge or just drop |
| class Data < Floe::Workflow::ChoiceRule | ||
| TYPES = ["String", "Numeric", "Boolean", "Timestamp", "Present", "Null"].freeze | ||
| COMPARES = ["Equals", "LessThan", "GreaterThan", "LessThanEquals", "GreaterThanEquals", "Matches"].freeze | ||
| OPERATIONS = TYPES.each_with_object({}) { |dt, a| a[dt] = :"is_#{dt.downcase}?" } \ |
There was a problem hiding this comment.
Line continuation is not needed
| OPERATIONS = TYPES.each_with_object({}) { |dt, a| a[dt] = :"is_#{dt.downcase}?" } \ | |
| OPERATIONS = TYPES.each_with_object({}) { |dt, a| a[dt] = :"is_#{dt.downcase}?" } |
There was a problem hiding this comment.
strange, thought I did this a while back. must have rebased it out of here.
It is back
| attr_reader :variable, :compare_key, :operation, :type, :compare_predicate, :path | ||
| attr_reader :variable, :compare_key, :operator, :type, :compare_predicate, :path |
There was a problem hiding this comment.
This is changing the "public" api - not a big deal if it's internal, but is anything else using this?
There was a problem hiding this comment.
we've been pretty lax on public and private api for these models.
Every attribute here is internal to this class only.
In a separate PR, do we want to circle through and mark private/public?
Would it buy us anything, or will it just make debugging more difficult?
Come to think of it, I think only Workflow and Context have a public api.
There was a problem hiding this comment.
No it's fine, I just wanted to make sure that was accounted for or not.
|
This PR got renamed? Having trouble following the history here. |
|
This was a PR with a ton of stuff. almost all of the stuff had been extracted (as you can see from the punch list at the top) Originally, the type checks ( I am not a fan of the non-grep style of update:
|
| elsif (match_value = TYPE_CHECK.match(key)) | ||
| @compare_key = key | ||
| _operator, type = match_value.captures | ||
| _is, @operator = match_value.captures |
There was a problem hiding this comment.
If we don't need the Is, you can also just not capture it in the first place. i.e.
- TYPE_CHECK = /^(Is)(#{TYPES.join("|")})$/
+ TYPE_CHECK = /^Is(#{TYPES.join("|")})$/There was a problem hiding this comment.
@Fryguy Yes, totally agree that the Is does not need to be captured, and it is probably more resource intensive.
I like the parallelism:
- operations has
(String)(Equals),(String)(GreaterThan)(Path). - type check has
(Is)(String)
Where we have a type (i.e.: String) and a verb (e.g.: Is, Equals, GreaterThan).
| OPERATIONS = TYPES.each_with_object({}) { |dt, a| a[dt] = :"is_#{dt.downcase}?" } \ | ||
| .merge(COMPARES.each_with_object({}) { |op, a| a[op] = :"op_#{op.downcase}?" }).freeze | ||
| # e.g.: (Is)(String), (Is)(Present) | ||
| TYPE_CHECK = /^(Is)(#{TYPES.join("|")})$/ |
There was a problem hiding this comment.
I just noticed this now, so not for this PR, but instead of TYPES.join(|), you should probably use Regex.union(TYPES). Same goes for the OPERATION constant
There was a problem hiding this comment.
irb(main):006> /Is(#{['a', 'b'].join("|")})/
=> /Is(a|b)/
irb(main):008> /Is(#{Regexp.union(['a', 'b'])})/
=> /Is((?-mix:a|b))/Are these the same thing?
There was a problem hiding this comment.
I have a separate PR all set for this
|
I'm fine with the PR rename now that the scope has been narrowed - it was just confusing because the opening PR had this list of dependent PRs that seemed unrelated. |
|
@Fryguy I think there is a question that asks, do we want to even do this? A while ago, we used to have hashes that were manually created: I liked that simple lookup. Currently, those are arrays not hashes and we manually generate the methods on the fly. This PR suggests auto generating a hash, but maybe that is just overkill/un-necessary. Do we think a static hash makes this any more sense/grepable/readable? Or is there another way to make this more accessible? (Just put the operation comment at the beginning of every op_gt? kind of method?). Performance wise, allocation difference will be negligible. Not sure the best way to benchmark this - but doesn't seem worth the work to gauge this. |
|
update
|
alt to #187
Depend upon:
[WIP] Catch and display path errors #246Overview
Remove un-necessary string allocations. and symbol lookups
Most of the changes have already been pulled out and merged.
So if we don't like this, then that is fine.
Leaving this for legacy reasons
Overview
Choice does not quite behave the same as the AWS States Language reference implementation.
If you specify a
Paththat points to nothing or the wrong type, aws raises aStates.Runtime. In truth, the type checking is not the most consistent, but the missing path is always raised.Changes:
Nextpoints to a valid state.Variableand compare key values are the correct data type.Variableor compare key path values not not found or the wrong data type. It used to throw ruby exceptions."IsPresent": trueto detect values present rather than not null."Is*"comparisons now supporttrueandfalsevalues.Choicewith noDefaultprovided.