Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
ea2e936
Add OpenFeature component
Strech Oct 22, 2025
034e5ee
Add openfeature-sdk stub and fix component typing
Strech Oct 24, 2025
04d3ad9
Add testing matrix and new group
Strech Oct 28, 2025
7b4f9c3
Update component initialization
Strech Nov 4, 2025
77e2877
Add InternalEvaluator class skeleton for functional flag evaluation
sameerank Nov 4, 2025
a81efa8
Add JSON parsing validation to InternalEvaluator with error codes
sameerank Nov 5, 2025
0edfdfe
Standardize InternalEvaluator error messages with libdatadog
sameerank Nov 5, 2025
c60f65e
Implement UFC configuration parsing with comprehensive Ruby data stru…
sameerank Nov 5, 2025
4a1b30c
Implement flag lookup with libdatadog-compatible error handling
sameerank Nov 5, 2025
0d983e4
Use allocation and variation data in InternalEvaluator
sameerank Nov 5, 2025
0c665c4
Integrate user defaults through OpenFeature evaluation chain
sameerank Nov 5, 2025
acb430f
Implement allocation matching logic with time bounds and user defaults
sameerank Nov 5, 2025
63fc4be
Implement comprehensive rule evaluation for allocation target
sameerank Nov 5, 2025
0f8ac83
Implement functional InternalEvaluator with 98.1% reference compatibiity
sameerank Nov 5, 2025
82d25cb
Fix InternalEvaluator compatibility and resolve class conflicts
sameerank Nov 6, 2025
95e829d
Align Ruby evaluator with libdatadog FFE interface contract
sameerank Nov 6, 2025
e624819
Refactor InternalEvaluator and remove deprecated Evaluator
sameerank Nov 6, 2025
7ff97cb
Remove time parameter from get_assignment to align with libdatadog FFI
sameerank Nov 7, 2025
8d04b6f
Update InternalEvaluator to match NativeEvaluator schema and configur…
sameerank Nov 7, 2025
789d957
Update RBS type signatures for OpenFeature binding refactor
sameerank Nov 7, 2025
2ee6e5a
Add test coverage for InternalEvaluator using fixture files
sameerank Nov 7, 2025
2a93836
Clarify UFC acronym throughout codebase for better developer understa…
sameerank Nov 10, 2025
980c5b5
Rename from_json to from_hash and reorder parameters
sameerank Nov 10, 2025
1a5501f
Remove unused legacy format support and dead code
sameerank Nov 10, 2025
789c7eb
Improve Ruby idioms by using Hash#fetch with defaults
sameerank Nov 10, 2025
52caa65
Add validation for required variationType field
sameerank Nov 10, 2025
42daa99
Improve Variation class documentation
sameerank Nov 10, 2025
41374d4
Use idiomatic Ruby constructors
sameerank Nov 10, 2025
51bc6b7
Remove redundant nil check in parse_timestamp (handled by else case)
sameerank Nov 10, 2025
c0ed3b3
Use fetch for consistently present fields to enforce validation
sameerank Nov 10, 2025
ed3da36
Replace the end method definition with an alias for end_value
sameerank Nov 10, 2025
591ef5d
Remove unnecessary parse_condition_value method and update RBS signat…
sameerank Nov 10, 2025
1e6df2c
Extract constant modules into separate files for better organization
sameerank Nov 10, 2025
15c91f9
Restore core OpenFeature evaluation functionality
sameerank Nov 10, 2025
4133b88
Clean up rebase artifacts and align with base branch
sameerank Nov 10, 2025
4affbc4
More clean up rebase artifacts and align with base branch expectations
sameerank Nov 10, 2025
e4c8588
Remove default_value and time arguments from evaluator get_assignment…
sameerank Nov 10, 2025
040feef
Remove duplicate ResolutionDetails and consolidate to shared implemen…
sameerank Nov 10, 2025
f5dc791
Remove default_value parameter from OpenFeature evaluator methods
sameerank Nov 10, 2025
7854fc1
Fix error message handling to align with libdatadog conventions
sameerank Nov 11, 2025
f7ae0b5
Fix OpenFeature evaluator to align with libdatadog behavior and pass …
sameerank Nov 11, 2025
f75815a
Fix OpenFeature error_code logic for correct provider behavior
sameerank Nov 11, 2025
9cd67f1
Add libdatadog FFI Reason enum support to OpenFeature binding
sameerank Nov 11, 2025
7446ddb
Consolidate OpenFeature binding tests and complete libdatadog alignment
sameerank Nov 11, 2025
f56b15b
Revert unnecessary provider error code conversion changes
sameerank Nov 11, 2025
4e8ae67
Fix OpenFeature binding test failures after consolidation
sameerank Nov 11, 2025
4ab7c54
Fix remaining OpenFeature test failures and complete integration
sameerank Nov 11, 2025
a3d0437
Clean up artifacts from rebase
sameerank Nov 11, 2025
0073ddc
Fix OpenFeature JSON parsing to support nested UFC format
sameerank Nov 11, 2025
b8a886a
Simplify OpenFeature JSON format to use flags at root level
sameerank Nov 11, 2025
c0089ea
Standardize OpenFeature flag configuration format and metadata
sameerank Nov 12, 2025
c50d134
Refactor for three-case evaluation model
sameerank Nov 12, 2025
ea1a6ad
Move error constants to binding module and more 3-case evaluation mod…
sameerank Nov 12, 2025
a586c90
Replace error code mapping with string error codes and enhance test v…
sameerank Nov 12, 2025
7e0e007
Fix attribute lookup for falsy values and remove dead symbol key code
sameerank Nov 12, 2025
02d3653
Clean up internal evaluator comments
sameerank Nov 12, 2025
b709060
Add Ruby-idiomatic query methods to ResolutionDetails
sameerank Nov 12, 2025
6c9e781
Add RBS type signatures for OpenFeature binding modules
sameerank Nov 12, 2025
4b70095
Optimize OpenFeature binding performance and validation
sameerank Nov 12, 2025
c616a6e
Fix to support OpenFeature EvaluationContext interface
sameerank Nov 12, 2025
6837078
Remove variationType from flag_metadata in OpenFeature evaluator
sameerank Nov 12, 2025
d4ea531
Simplify internal evaluator to only accept hash contexts
sameerank Nov 12, 2025
d234648
Update OpenFeature binding to accept string expected_type parameters
sameerank Nov 13, 2025
6bcb2ef
Remove result? method and return default_value for all cases without …
sameerank Nov 13, 2025
c634bca
Fix Standard RB linting issues in binding files
sameerank Nov 13, 2025
94b3670
Use DEFAULT_ALLOCATION_NULL for missing variation references
sameerank Nov 13, 2025
c73de59
Reorder get_assignment method parameters
sameerank Nov 13, 2025
8e9e16a
Integrate InternalEvaluator with EvaluationEngine
sameerank Nov 14, 2025
400558a
Clean up comments and integration
sameerank Nov 14, 2025
48e6752
Refactor ResolutionDetails creation to use factory methods
sameerank Nov 14, 2025
09cfe10
Add type safety and fix Steep type errors in InternalEvaluator
sameerank Nov 14, 2025
2bae8a0
Update MD5 hash comment
sameerank Nov 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions lib/datadog/open_feature/binding/assignment_reason.rb
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
Comment on lines 6 to 13
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor Author

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

pub enum Reason {
    Static,
    Default,
    TargetingMatch,
    Split,
    Disabled,
    Error,
}

https://github.com/DataDog/libdatadog/blob/3d9e641016f3a6c05bfdb1786c4b1a84342d4bbf/datadog-ffe-ffi/src/assignment.rs#L373-L391

impl From<&ResolutionDetails> for Reason {
    fn from(value: &ResolutionDetails) -> Self {
        match value.as_ref() {
            Ok(assignment) => assignment.reason.into(),
            Err(EvaluationError::FlagDisabled) => Reason::Disabled,
            Err(EvaluationError::DefaultAllocationNull) => Reason::Default,
            Err(_) => Reason::Error,
        }
    }
}
impl From<AssignmentReason> for Reason {
    fn from(value: AssignmentReason) -> Self {
        match value {
            AssignmentReason::TargetingMatch => Reason::TargetingMatch,
            AssignmentReason::Split => Reason::Split,
            AssignmentReason::Static => Reason::Static,
        }
    }
}

end
end
end
19 changes: 19 additions & 0 deletions lib/datadog/open_feature/binding/condition_operator.rb
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
248 changes: 248 additions & 0 deletions lib/datadog/open_feature/binding/configuration.rb
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
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
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
Copy link
Member

Choose a reason for hiding this comment

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

TrafficSplit?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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
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
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
17 changes: 17 additions & 0 deletions lib/datadog/open_feature/binding/error_codes.rb
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
Loading
Loading