-
Notifications
You must be signed in to change notification settings - Fork 399
[FFL-1361] Evaluation in binding in ruby #5022
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
Changes from all commits
ea2e936
034e5ee
04d3ad9
7b4f9c3
77e2877
a81efa8
0edfdfe
c60f65e
4a1b30c
0d983e4
0c665c4
acb430f
63fc4be
0f8ac83
82d25cb
95e829d
e624819
7ff97cb
8d04b6f
789d957
2ee6e5a
2a93836
980c5b5
1a5501f
789c7eb
52caa65
42daa99
41374d4
51bc6b7
c0ed3b3
ed3da36
591ef5d
1e6df2c
15c91f9
4133b88
4affbc4
e4c8588
040feef
f5dc791
7854fc1
f7ae0b5
f75815a
9cd67f1
7446ddb
f56b15b
4e8ae67
4ab7c54
a3d0437
0073ddc
b8a886a
c0089ea
c50d134
ea1a6ad
a586c90
7e0e007
02d3653
b709060
6c9e781
4b70095
c616a6e
6837078
d4ea531
d234648
6bcb2ef
c634bca
94b3670
c73de59
8e9e16a
400558a
48e6752
09cfe10
2bae8a0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module Datadog | ||
| module OpenFeature | ||
| module Binding | ||
| module AssignmentReason | ||
| TARGETING_MATCH = 'TARGETING_MATCH' | ||
| SPLIT = 'SPLIT' | ||
| STATIC = 'STATIC' | ||
| DEFAULT = 'DEFAULT' | ||
| DISABLED = 'DISABLED' | ||
| ERROR = 'ERROR' | ||
| end | ||
| end | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module Datadog | ||
| module OpenFeature | ||
| module Binding | ||
| module ConditionOperator | ||
| MATCHES = 'MATCHES' | ||
| NOT_MATCHES = 'NOT_MATCHES' | ||
| GTE = 'GTE' | ||
| GT = 'GT' | ||
| LTE = 'LTE' | ||
| LT = 'LT' | ||
| ONE_OF = 'ONE_OF' | ||
| NOT_ONE_OF = 'NOT_ONE_OF' | ||
| IS_NULL = 'IS_NULL' | ||
| end | ||
| end | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -0,0 +1,248 @@ | ||||
| # frozen_string_literal: true | ||||
|
|
||||
| require_relative 'variation_type' | ||||
| require_relative 'condition_operator' | ||||
| require_relative 'assignment_reason' | ||||
|
|
||||
| module Datadog | ||||
| module OpenFeature | ||||
| module Binding | ||||
| class Flag | ||||
| attr_reader :key, :enabled, :variation_type, :variations, :allocations | ||||
|
|
||||
| def initialize(key:, enabled:, variation_type:, variations:, allocations:) | ||||
| @key = key | ||||
| @enabled = enabled | ||||
| @variation_type = variation_type | ||||
| @variations = variations | ||||
| @allocations = allocations | ||||
| end | ||||
|
|
||||
| def self.from_hash(flag_data, key) | ||||
| new( | ||||
| key: key, | ||||
| enabled: flag_data.fetch('enabled', false), | ||||
| variation_type: flag_data.fetch('variationType'), | ||||
| variations: parse_variations(flag_data.fetch('variations', {})), | ||||
| allocations: parse_allocations(flag_data.fetch('allocations', [])) | ||||
| ) | ||||
| end | ||||
|
|
||||
| def self.parse_variations(variations_data) | ||||
| variations_data.transform_values do |variation_data| | ||||
| Variation.from_hash(variation_data) | ||||
| end | ||||
| end | ||||
|
|
||||
| def self.parse_allocations(allocations_data) | ||||
| allocations_data.map { |allocation_data| Allocation.from_hash(allocation_data) } | ||||
| end | ||||
|
|
||||
| private_class_method :parse_variations, :parse_allocations | ||||
| end | ||||
|
|
||||
| # Represents a flag variation with a key for logging and a value for the application | ||||
| class Variation | ||||
sameerank marked this conversation as resolved.
Show resolved
Hide resolved
|
||||
| attr_reader :key, :value | ||||
|
|
||||
| def initialize(key:, value:) | ||||
| @key = key | ||||
| @value = value | ||||
| end | ||||
|
|
||||
| def self.from_hash(variation_data) | ||||
| new( | ||||
| key: variation_data.fetch('key'), | ||||
| value: variation_data.fetch('value') | ||||
| ) | ||||
| end | ||||
| end | ||||
|
|
||||
| # Represents an allocation rule with traffic splits | ||||
| class Allocation | ||||
sameerank marked this conversation as resolved.
Show resolved
Hide resolved
|
||||
| attr_reader :key, :rules, :start_at, :end_at, :splits, :do_log | ||||
|
|
||||
| def initialize(key:, splits:, rules: nil, start_at: nil, end_at: nil, do_log: true) | ||||
| @key = key | ||||
| @rules = rules | ||||
| @start_at = start_at | ||||
| @end_at = end_at | ||||
| @splits = splits | ||||
| @do_log = do_log | ||||
| end | ||||
|
|
||||
| def self.from_hash(allocation_data) | ||||
| new( | ||||
| key: allocation_data.fetch('key'), | ||||
| rules: parse_rules(allocation_data['rules']), | ||||
| start_at: parse_timestamp(allocation_data['startAt']), | ||||
| end_at: parse_timestamp(allocation_data['endAt']), | ||||
| splits: parse_splits(allocation_data.fetch('splits', [])), | ||||
| do_log: allocation_data.fetch('doLog', true) | ||||
| ) | ||||
| end | ||||
|
|
||||
| def self.parse_rules(rules_data) | ||||
| return nil if rules_data.nil? || rules_data.empty? | ||||
|
|
||||
| rules_data.map { |rule_data| Rule.from_hash(rule_data) } | ||||
| end | ||||
|
|
||||
| def self.parse_splits(splits_data) | ||||
| splits_data.map { |split_data| Split.from_hash(split_data) } | ||||
| end | ||||
|
|
||||
| def self.parse_timestamp(timestamp_data) | ||||
| # Handle both Unix timestamps and ISO8601 strings | ||||
| case timestamp_data | ||||
| when Numeric | ||||
| Time.at(timestamp_data) | ||||
| when String | ||||
| Time.parse(timestamp_data) | ||||
| end | ||||
| rescue | ||||
| nil | ||||
| end | ||||
|
|
||||
| private_class_method :parse_rules, :parse_splits, :parse_timestamp | ||||
| end | ||||
|
|
||||
| # Represents a traffic split within an allocation | ||||
| class Split | ||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it'll be easier to see how this corresponds to the fields with the same names in the JSON if the key and class names match, e.g. dd-trace-rb/spec/fixtures/ufc/flags-v1.json Line 728 in 49dafd3
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tbh I think if you are adding an abstraction over this data to avoid working with a big Hash directly, it makes no sense to duplicate key names 1:1 to class names. The class names should make sense on their own, since it helps with overall code readability. But I guess this is your decision in the end, especially if this code is temporary as you mentioned. |
||||
| attr_reader :shards, :variation_key, :extra_logging | ||||
|
|
||||
| def initialize(shards:, variation_key:, extra_logging: nil) | ||||
| @shards = shards | ||||
| @variation_key = variation_key | ||||
| @extra_logging = extra_logging || {} | ||||
| end | ||||
|
|
||||
| def self.from_hash(split_data) | ||||
| new( | ||||
| shards: parse_shards(split_data.fetch('shards', [])), | ||||
| variation_key: split_data.fetch('variationKey'), | ||||
| extra_logging: split_data.fetch('extraLogging', {}) | ||||
| ) | ||||
| end | ||||
|
|
||||
| def self.parse_shards(shards_data) | ||||
| shards_data.map { |shard_data| Shard.from_hash(shard_data) } | ||||
| end | ||||
|
|
||||
| private_class_method :parse_shards | ||||
| end | ||||
|
|
||||
| # Represents a shard configuration for traffic splitting | ||||
| class Shard | ||||
sameerank marked this conversation as resolved.
Show resolved
Hide resolved
|
||||
| attr_reader :salt, :total_shards, :ranges | ||||
|
|
||||
| def initialize(salt:, total_shards:, ranges:) | ||||
| @salt = salt | ||||
| @total_shards = total_shards | ||||
| @ranges = ranges | ||||
| end | ||||
|
|
||||
| def self.from_hash(shard_data) | ||||
| new( | ||||
| salt: shard_data.fetch('salt'), | ||||
| total_shards: shard_data.fetch('totalShards'), | ||||
| ranges: parse_ranges(shard_data.fetch('ranges', [])) | ||||
| ) | ||||
| end | ||||
|
|
||||
| def self.parse_ranges(ranges_data) | ||||
| ranges_data.map { |range_data| ShardRange.from_hash(range_data) } | ||||
| end | ||||
|
|
||||
| private_class_method :parse_ranges | ||||
| end | ||||
|
|
||||
| # Represents a shard range for traffic allocation | ||||
| class ShardRange | ||||
| attr_reader :start, :end_value | ||||
|
|
||||
| def initialize(start:, end_value:) | ||||
| @start = start | ||||
| @end_value = end_value | ||||
| end | ||||
|
|
||||
| def self.from_hash(range_data) | ||||
| new( | ||||
| start: range_data.fetch('start'), | ||||
| end_value: range_data.fetch('end') | ||||
| ) | ||||
| end | ||||
|
|
||||
| # Alias because "end" is a reserved keyword in Ruby | ||||
| alias_method :end, :end_value | ||||
| end | ||||
|
|
||||
| # Represents a targeting rule | ||||
| class Rule | ||||
sameerank marked this conversation as resolved.
Show resolved
Hide resolved
|
||||
| attr_reader :conditions | ||||
|
|
||||
| def initialize(conditions:) | ||||
| @conditions = conditions | ||||
| end | ||||
|
|
||||
| def self.from_hash(rule_data) | ||||
| new( | ||||
| conditions: parse_conditions(rule_data.fetch('conditions', [])) | ||||
| ) | ||||
| end | ||||
|
|
||||
| def self.parse_conditions(conditions_data) | ||||
| conditions_data.map { |condition_data| Condition.from_hash(condition_data) } | ||||
| end | ||||
|
|
||||
| private_class_method :parse_conditions | ||||
| end | ||||
|
|
||||
| # Represents a single condition within a rule | ||||
| class Condition | ||||
| attr_reader :attribute, :operator, :value | ||||
|
|
||||
| def initialize(attribute:, operator:, value:) | ||||
| @attribute = attribute | ||||
| @operator = operator | ||||
| @value = value | ||||
| end | ||||
|
|
||||
| def self.from_hash(condition_data) | ||||
| new( | ||||
| attribute: condition_data.fetch('attribute'), | ||||
| operator: condition_data.fetch('operator'), | ||||
| value: condition_data.fetch('value') | ||||
| ) | ||||
| end | ||||
| end | ||||
|
|
||||
| # Main configuration container | ||||
| class Configuration | ||||
| attr_reader :flags, :schema_version | ||||
|
|
||||
| def initialize(flags:, schema_version: nil) | ||||
| @flags = flags | ||||
| @schema_version = schema_version | ||||
| end | ||||
|
|
||||
| def self.from_hash(config_data) | ||||
| flags_data = config_data.fetch('flags', {}) | ||||
|
|
||||
| parsed_flags = flags_data.transform_values do |flag_data| | ||||
| Flag.from_hash(flag_data, flag_data['key'] || '') | ||||
| end | ||||
|
|
||||
| new( | ||||
| flags: parsed_flags, | ||||
| schema_version: config_data['schemaVersion'] | ||||
| ) | ||||
| end | ||||
|
|
||||
| def get_flag(flag_key) | ||||
| @flags.values.find { |flag| flag.key == flag_key } | ||||
| end | ||||
| end | ||||
| end | ||||
| end | ||||
| end | ||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module Datadog | ||
| module OpenFeature | ||
| module Binding | ||
| module ErrorCodes | ||
| TYPE_MISMATCH_ERROR = 'TYPE_MISMATCH' | ||
| CONFIGURATION_PARSE_ERROR = 'CONFIGURATION_PARSE_ERROR' | ||
| CONFIGURATION_MISSING = 'CONFIGURATION_MISSING' | ||
| FLAG_UNRECOGNIZED_OR_DISABLED = 'FLAG_UNRECOGNIZED_OR_DISABLED' | ||
| FLAG_DISABLED = 'FLAG_DISABLED' | ||
| DEFAULT_ALLOCATION_NULL = 'DEFAULT_ALLOCATION_NULL' | ||
| INTERNAL_ERROR = 'INTERNAL' | ||
| end | ||
| end | ||
| end | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All caps due to
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]https://github.com/DataDog/libdatadog/blob/b78a66bcf487afbe46c4e409834cecbc882ef01d/datadog-ffe/src/rules_based/ufc/assignment.rs#L13-L21
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://github.com/DataDog/libdatadog/blob/3d9e641016f3a6c05bfdb1786c4b1a84342d4bbf/datadog-ffe-ffi/src/assignment.rs#L144
https://github.com/DataDog/libdatadog/blob/3d9e641016f3a6c05bfdb1786c4b1a84342d4bbf/datadog-ffe-ffi/src/assignment.rs#L373-L391