build: change checkstyle to google code format, plus adding spotless (#1264)
Signed-off-by: Simon Schrottner <simon.schrottner@dynatrace.com>
This commit is contained in:
parent
f1817d8fef
commit
64ec68bcf5
|
|
@ -0,0 +1,72 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
tab_width = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
ij_continuation_indent_size = 8
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
# Following the rules of the Google Java Style Guide.
|
||||
# See https://google.github.io/styleguide/javaguide.html
|
||||
[*.java]
|
||||
max_line_length = 120
|
||||
|
||||
ij_java_do_not_wrap_after_single_annotation_in_parameter = true
|
||||
ij_java_insert_inner_class_imports = false
|
||||
ij_java_class_count_to_use_import_on_demand = 999
|
||||
ij_java_names_count_to_use_import_on_demand = 999
|
||||
ij_java_packages_to_use_import_on_demand = unset
|
||||
ij_java_imports_layout = $*,|,*
|
||||
ij_java_doc_align_param_comments = true
|
||||
ij_java_doc_align_exception_comments = true
|
||||
ij_java_doc_add_p_tag_on_empty_lines = false
|
||||
ij_java_doc_do_not_wrap_if_one_line = true
|
||||
ij_java_doc_keep_empty_parameter_tag = false
|
||||
ij_java_doc_keep_empty_throws_tag = false
|
||||
ij_java_doc_keep_empty_return_tag = false
|
||||
ij_java_doc_preserve_line_breaks = true
|
||||
ij_java_doc_indent_on_continuation = true
|
||||
ij_java_keep_control_statement_in_one_line = false
|
||||
ij_java_keep_blank_lines_in_code = 1
|
||||
ij_java_align_multiline_parameters = false
|
||||
ij_java_align_multiline_resources = false
|
||||
ij_java_align_multiline_for = true
|
||||
ij_java_space_before_array_initializer_left_brace = true
|
||||
ij_java_call_parameters_wrap = normal
|
||||
ij_java_method_parameters_wrap = normal
|
||||
ij_java_extends_list_wrap = normal
|
||||
ij_java_throws_keyword_wrap = normal
|
||||
ij_java_method_call_chain_wrap = normal
|
||||
ij_java_binary_operation_wrap = normal
|
||||
ij_java_binary_operation_sign_on_next_line = true
|
||||
ij_java_ternary_operation_wrap = normal
|
||||
ij_java_ternary_operation_signs_on_next_line = true
|
||||
ij_java_keep_simple_methods_in_one_line = true
|
||||
ij_java_keep_simple_lambdas_in_one_line = true
|
||||
ij_java_keep_simple_classes_in_one_line = true
|
||||
ij_java_for_statement_wrap = normal
|
||||
ij_java_array_initializer_wrap = normal
|
||||
ij_java_wrap_comments = true
|
||||
ij_java_if_brace_force = always
|
||||
ij_java_do_while_brace_force = always
|
||||
ij_java_while_brace_force = always
|
||||
ij_java_for_brace_force = always
|
||||
ij_java_space_after_closing_angle_bracket_in_type_argument = false
|
||||
|
||||
[{*.json,*.json5}]
|
||||
indent_size = 2
|
||||
tab_width = 2
|
||||
ij_smart_tabs = false
|
||||
|
||||
[*.yaml]
|
||||
indent_size = 2
|
||||
tab_width = 2
|
||||
|
|
@ -21,6 +21,38 @@ If you think we might be out of date with the spec, you can check that by invoki
|
|||
|
||||
If you're adding tests to cover something in the spec, use the `@Specification` annotation like you see throughout the test suites.
|
||||
|
||||
## Code Styles
|
||||
|
||||
### Overview
|
||||
Our project follows strict code formatting standards to maintain consistency and readability across the codebase. We use [Spotless](https://github.com/diffplug/spotless) integrated with the [Palantir Java Format](https://github.com/palantir/palantir-java-format) for code formatting.
|
||||
|
||||
**Spotless** ensures that all code complies with the formatting rules automatically, reducing style-related issues during code reviews.
|
||||
|
||||
### How to Format Your Code
|
||||
1. **Before Committing Changes:**
|
||||
Run the Spotless plugin to format your code. This will apply the Palantir Java Format style:
|
||||
```bash
|
||||
mvn spotless:apply
|
||||
```
|
||||
|
||||
2. **Verify Formatting:**
|
||||
To check if your code adheres to the style guidelines without making changes:
|
||||
```bash
|
||||
mvn spotless:check
|
||||
```
|
||||
|
||||
- If this command fails, your code does not follow the required formatting. Use `mvn spotless:apply` to fix it.
|
||||
|
||||
### CI/CD Integration
|
||||
Our Continuous Integration (CI) pipeline automatically checks code formatting using the Spotless plugin. Any code that does not pass the `spotless:check` step will cause the build to fail.
|
||||
|
||||
### Best Practices
|
||||
- Regularly run `mvn spotless:apply` during your work to ensure your code remains aligned with the standards.
|
||||
- Configure your IDE (e.g., IntelliJ IDEA or Eclipse) to follow the Palantir Java format guidelines to reduce discrepancies during development.
|
||||
|
||||
### Support
|
||||
If you encounter issues with code formatting, please raise a GitHub issue or contact the maintainers.
|
||||
|
||||
## End-to-End Tests
|
||||
|
||||
The continuous integration runs a set of [gherkin e2e tests](https://github.com/open-feature/spec/blob/main/specification/assets/gherkin/evaluation.feature) using `InMemoryProvider`.
|
||||
|
|
|
|||
261
checkstyle.xml
261
checkstyle.xml
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0"?>
|
||||
<!DOCTYPE module PUBLIC
|
||||
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
|
||||
"https://checkstyle.org/dtds/configuration_1_3.dtd">
|
||||
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
|
||||
"https://checkstyle.org/dtds/configuration_1_3.dtd">
|
||||
|
||||
<!--
|
||||
Checkstyle configuration that checks the Google coding conventions from Google Java Style
|
||||
|
|
@ -13,17 +13,18 @@
|
|||
To completely disable a check, just comment it out or delete it from the file.
|
||||
To suppress certain violations please review suppression filters.
|
||||
|
||||
Authors: Max Vetrenko, Ruslan Diachenko, Roman Ivanov.
|
||||
Authors: Max Vetrenko, Mauryan Kansara, Ruslan Diachenko, Roman Ivanov.
|
||||
-->
|
||||
|
||||
<module name = "Checker">
|
||||
<module name="Checker">
|
||||
|
||||
<property name="charset" value="UTF-8"/>
|
||||
|
||||
<property name="severity" value="error"/>
|
||||
|
||||
<property name="fileExtensions" value="java, properties, xml"/>
|
||||
<!-- Excludes all 'module-info.java' files -->
|
||||
<!-- See https://checkstyle.org/config_filefilters.html -->
|
||||
<!-- See https://checkstyle.org/filefilters/index.html -->
|
||||
<module name="BeforeExecutionExclusionFileFilter">
|
||||
<property name="fileNamePattern" value="module\-info\.java$"/>
|
||||
</module>
|
||||
|
|
@ -34,9 +35,16 @@
|
|||
<property name="optional" value="true"/>
|
||||
</module>
|
||||
|
||||
<!-- https://checkstyle.org/filters/suppresswithnearbytextfilter.html -->
|
||||
<!--<module name="SuppressWithNearbyTextFilter">
|
||||
<property name="nearbyTextPattern"
|
||||
value="CHECKSTYLE.SUPPRESS\: (\w+) for ([+-]\d+) lines"/>
|
||||
<property name="checkPattern" value="$1"/>
|
||||
<property name="lineRange" value="$2"/>
|
||||
</module>-->
|
||||
|
||||
<!-- Checks for whitespace -->
|
||||
<!-- See http://checkstyle.org/config_whitespace.html -->
|
||||
<!-- See http://checkstyle.org/checks/whitespace/index.html -->
|
||||
<module name="FileTabCharacter">
|
||||
<property name="eachLine" value="true"/>
|
||||
</module>
|
||||
|
|
@ -52,7 +60,7 @@
|
|||
<module name="TreeWalker">
|
||||
<!-- needed for SuppressWarningsFilter -->
|
||||
<module name="SuppressWarningsHolder" />
|
||||
|
||||
|
||||
<module name="SuppressWarnings">
|
||||
<property name="id" value="checkstyle:suppresswarnings"/>
|
||||
</module>
|
||||
|
|
@ -69,48 +77,68 @@
|
|||
<module name="IllegalTokenText">
|
||||
<property name="tokens" value="STRING_LITERAL, CHAR_LITERAL"/>
|
||||
<property name="format"
|
||||
value="\\u00(09|0(a|A)|0(c|C)|0(d|D)|22|27|5(C|c))|\\(0(10|11|12|14|15|42|47)|134)"/>
|
||||
value="\\u00(09|0(a|A)|0(c|C)|0(d|D)|22|27|5(C|c))|\\(0(10|11|12|14|15|42|47)|134)"/>
|
||||
<property name="message"
|
||||
value="Consider using special escape sequence instead of octal value or Unicode escaped value."/>
|
||||
value="Consider using special escape sequence instead of octal value or Unicode escaped value."/>
|
||||
</module>
|
||||
<module name="AvoidEscapedUnicodeCharacters">
|
||||
<property name="allowEscapesForControlCharacters" value="true"/>
|
||||
<property name="allowByTailComment" value="true"/>
|
||||
<property name="allowNonPrintableEscapes" value="true"/>
|
||||
</module>
|
||||
<module name="AvoidStarImport"/>
|
||||
<module name="OneTopLevelClass"/>
|
||||
<module name="NoLineWrap">
|
||||
<property name="tokens" value="PACKAGE_DEF, IMPORT, STATIC_IMPORT"/>
|
||||
</module>
|
||||
<module name="EmptyBlock">
|
||||
<property name="option" value="TEXT"/>
|
||||
<property name="tokens"
|
||||
value="LITERAL_TRY, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_SWITCH"/>
|
||||
</module>
|
||||
<module name="NeedBraces">
|
||||
<property name="tokens"
|
||||
value="LITERAL_DO, LITERAL_ELSE, LITERAL_FOR, LITERAL_IF, LITERAL_WHILE"/>
|
||||
value="LITERAL_DO, LITERAL_ELSE, LITERAL_FOR, LITERAL_IF, LITERAL_WHILE"/>
|
||||
</module>
|
||||
<module name="LeftCurly">
|
||||
<property name="id" value="LeftCurlyEol"/>
|
||||
<property name="tokens"
|
||||
value="ANNOTATION_DEF, CLASS_DEF, CTOR_DEF, ENUM_CONSTANT_DEF, ENUM_DEF,
|
||||
INTERFACE_DEF, LAMBDA, LITERAL_CASE, LITERAL_CATCH, LITERAL_DEFAULT,
|
||||
LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY, LITERAL_FOR, LITERAL_IF,
|
||||
LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_WHILE, METHOD_DEF,
|
||||
OBJBLOCK, STATIC_INIT"/>
|
||||
value="ANNOTATION_DEF, CLASS_DEF, CTOR_DEF, ENUM_CONSTANT_DEF, ENUM_DEF,
|
||||
INTERFACE_DEF, LITERAL_CATCH,
|
||||
LITERAL_DO, LITERAL_ELSE, LITERAL_FOR, LITERAL_IF,
|
||||
LITERAL_WHILE, METHOD_DEF,
|
||||
OBJBLOCK, STATIC_INIT, RECORD_DEF, COMPACT_CTOR_DEF"/>
|
||||
</module>
|
||||
<module name="LeftCurly">
|
||||
<property name="id" value="LeftCurlyNl"/>
|
||||
<property name="option" value="nl"/>
|
||||
<property name="tokens"
|
||||
value=" LITERAL_DEFAULT"/>
|
||||
</module>
|
||||
<module name="SuppressionXpathSingleFilter">
|
||||
<!-- LITERAL_DEFAULT are reused in SWITCH_RULE -->
|
||||
<property name="id" value="LeftCurlyNl"/>
|
||||
<property name="query" value="//SWITCH_RULE/SLIST"/>
|
||||
</module>
|
||||
<module name="RightCurly">
|
||||
<property name="id" value="RightCurlySame"/>
|
||||
<property name="tokens"
|
||||
value="LITERAL_TRY, LITERAL_CATCH, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE,
|
||||
value="LITERAL_IF, LITERAL_ELSE,
|
||||
LITERAL_DO"/>
|
||||
</module>
|
||||
<module name="RightCurly">
|
||||
<property name="id" value="RightCurlyAlone"/>
|
||||
<property name="option" value="alone"/>
|
||||
<property name="tokens"
|
||||
value="CLASS_DEF, METHOD_DEF, CTOR_DEF, LITERAL_FOR, LITERAL_WHILE, STATIC_INIT,
|
||||
INSTANCE_INIT, ANNOTATION_DEF, ENUM_DEF"/>
|
||||
value="CLASS_DEF, METHOD_DEF, CTOR_DEF, LITERAL_FOR, LITERAL_WHILE, STATIC_INIT,
|
||||
INSTANCE_INIT, ANNOTATION_DEF, ENUM_DEF, INTERFACE_DEF, RECORD_DEF,
|
||||
COMPACT_CTOR_DEF"/>
|
||||
</module>
|
||||
<module name="SuppressionXpathSingleFilter">
|
||||
<!-- suppression is required till https://github.com/checkstyle/checkstyle/issues/7541 -->
|
||||
<property name="id" value="RightCurlyAlone"/>
|
||||
<property name="query" value="//RCURLY[parent::SLIST[count(./*)=1]
|
||||
or preceding-sibling::*[last()][self::LCURLY]]"/>
|
||||
</module>
|
||||
<module name="WhitespaceAfter">
|
||||
<property name="tokens"
|
||||
value="COMMA, SEMI, TYPECAST, LITERAL_IF, LITERAL_ELSE,
|
||||
LITERAL_WHILE, LITERAL_DO, LITERAL_FOR, DO_WHILE"/>
|
||||
</module>
|
||||
<module name="WhitespaceAround">
|
||||
<property name="allowEmptyConstructors" value="true"/>
|
||||
|
|
@ -118,18 +146,35 @@
|
|||
<property name="allowEmptyMethods" value="true"/>
|
||||
<property name="allowEmptyTypes" value="true"/>
|
||||
<property name="allowEmptyLoops" value="true"/>
|
||||
<!--<property name="allowEmptySwitchBlockStatements" value="true"/>-->
|
||||
<property name="ignoreEnhancedForColon" value="false"/>
|
||||
<property name="tokens"
|
||||
value="ASSIGN, BAND, BAND_ASSIGN, BOR, BOR_ASSIGN, BSR, BSR_ASSIGN, BXOR,
|
||||
BXOR_ASSIGN, COLON, DIV, DIV_ASSIGN, DO_WHILE, EQUAL, GE, GT, LAMBDA, LAND,
|
||||
LCURLY, LE, LITERAL_CATCH, LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY,
|
||||
LITERAL_FOR, LITERAL_IF, LITERAL_RETURN, LITERAL_SWITCH, LITERAL_SYNCHRONIZED,
|
||||
LITERAL_TRY, LITERAL_WHILE, LOR, LT, MINUS, MINUS_ASSIGN, MOD, MOD_ASSIGN,
|
||||
NOT_EQUAL, PLUS, PLUS_ASSIGN, QUESTION, RCURLY, SL, SLIST, SL_ASSIGN, SR,
|
||||
SR_ASSIGN, STAR, STAR_ASSIGN, LITERAL_ASSERT, TYPE_EXTENSION_AND"/>
|
||||
value="ASSIGN, BAND, BAND_ASSIGN, BOR, BOR_ASSIGN, BSR, BSR_ASSIGN, BXOR,
|
||||
BXOR_ASSIGN, COLON, DIV, DIV_ASSIGN, DO_WHILE, EQUAL, GE, GT, LAND,
|
||||
LCURLY, LE, LITERAL_DO, LITERAL_ELSE,
|
||||
LITERAL_FOR, LITERAL_IF,
|
||||
LITERAL_WHILE, LOR, LT, MINUS, MINUS_ASSIGN, MOD, MOD_ASSIGN,
|
||||
NOT_EQUAL, PLUS, PLUS_ASSIGN, QUESTION, RCURLY, SL, SLIST, SL_ASSIGN, SR,
|
||||
SR_ASSIGN, STAR, STAR_ASSIGN, LITERAL_ASSERT,
|
||||
TYPE_EXTENSION_AND"/>
|
||||
<message key="ws.notFollowed"
|
||||
value="WhitespaceAround: ''{0}'' is not followed by whitespace. Empty blocks may only be represented as '{}' when not part of a multi-block statement (4.1.3)"/>
|
||||
value="WhitespaceAround: ''{0}'' is not followed by whitespace. Empty blocks
|
||||
may only be represented as '{}' when not part of a multi-block statement (4.1.3)"/>
|
||||
<message key="ws.notPreceded"
|
||||
value="WhitespaceAround: ''{0}'' is not preceded with whitespace."/>
|
||||
value="WhitespaceAround: ''{0}'' is not preceded with whitespace."/>
|
||||
</module>
|
||||
<module name="SuppressionXpathSingleFilter">
|
||||
<property name="checks" value="WhitespaceAround"/>
|
||||
<property name="query" value="//*[self::LITERAL_IF or self::LITERAL_ELSE or self::STATIC_INIT
|
||||
or self::LITERAL_TRY or self::LITERAL_CATCH]/SLIST[count(./*)=1]
|
||||
| //*[self::STATIC_INIT or self::LITERAL_TRY or self::LITERAL_IF]
|
||||
//*[self::RCURLY][parent::SLIST[count(./*)=1]]"/>
|
||||
</module>
|
||||
<module name="RegexpSinglelineJava">
|
||||
<property name="format" value="\{[ ]+\}"/>
|
||||
<property name="message" value="Empty blocks should have no spaces. Empty blocks
|
||||
may only be represented as '{}' when not part of a
|
||||
multi-block statement (4.1.3)"/>
|
||||
</module>
|
||||
<module name="OneStatementPerLine"/>
|
||||
<module name="MultipleVariableDeclarations"/>
|
||||
|
|
@ -140,8 +185,9 @@
|
|||
<module name="ModifierOrder"/>
|
||||
<module name="EmptyLineSeparator">
|
||||
<property name="tokens"
|
||||
value="PACKAGE_DEF, IMPORT, STATIC_IMPORT, CLASS_DEF, INTERFACE_DEF, ENUM_DEF,
|
||||
STATIC_INIT, INSTANCE_INIT, METHOD_DEF, CTOR_DEF, VARIABLE_DEF"/>
|
||||
value="PACKAGE_DEF, IMPORT, STATIC_IMPORT, CLASS_DEF, INTERFACE_DEF, ENUM_DEF,
|
||||
STATIC_INIT, INSTANCE_INIT, METHOD_DEF, CTOR_DEF, VARIABLE_DEF, RECORD_DEF,
|
||||
COMPACT_CTOR_DEF"/>
|
||||
<property name="allowNoEmptyLineBetweenFields" value="true"/>
|
||||
</module>
|
||||
<module name="SeparatorWrap">
|
||||
|
|
@ -155,13 +201,13 @@
|
|||
<property name="option" value="EOL"/>
|
||||
</module>
|
||||
<module name="SeparatorWrap">
|
||||
<!-- ELLIPSIS is EOL until https://github.com/google/styleguide/issues/258 -->
|
||||
<!-- ELLIPSIS is EOL until https://github.com/google/styleguide/issues/259 -->
|
||||
<property name="id" value="SeparatorWrapEllipsis"/>
|
||||
<property name="tokens" value="ELLIPSIS"/>
|
||||
<property name="option" value="EOL"/>
|
||||
</module>
|
||||
<module name="SeparatorWrap">
|
||||
<!-- ARRAY_DECLARATOR is EOL until https://github.com/google/styleguide/issues/259 -->
|
||||
<!-- ARRAY_DECLARATOR is EOL until https://github.com/google/styleguide/issues/258 -->
|
||||
<property name="id" value="SeparatorWrapArrayDeclarator"/>
|
||||
<property name="tokens" value="ARRAY_DECLARATOR"/>
|
||||
<property name="option" value="EOL"/>
|
||||
|
|
@ -174,22 +220,23 @@
|
|||
<module name="PackageName">
|
||||
<property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$"/>
|
||||
<message key="name.invalidPattern"
|
||||
value="Package name ''{0}'' must match pattern ''{1}''."/>
|
||||
value="Package name ''{0}'' must match pattern ''{1}''."/>
|
||||
</module>
|
||||
<module name="TypeName">
|
||||
<property name="tokens" value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, ANNOTATION_DEF"/>
|
||||
<property name="tokens" value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF,
|
||||
ANNOTATION_DEF, RECORD_DEF"/>
|
||||
<message key="name.invalidPattern"
|
||||
value="Type name ''{0}'' must match pattern ''{1}''."/>
|
||||
value="Type name ''{0}'' must match pattern ''{1}''."/>
|
||||
</module>
|
||||
<module name="MemberName">
|
||||
<property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9]*$"/>
|
||||
<message key="name.invalidPattern"
|
||||
value="Member name ''{0}'' must match pattern ''{1}''."/>
|
||||
value="Member name ''{0}'' must match pattern ''{1}''."/>
|
||||
</module>
|
||||
<module name="ParameterName">
|
||||
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
|
||||
<message key="name.invalidPattern"
|
||||
value="Parameter name ''{0}'' must match pattern ''{1}''."/>
|
||||
value="Parameter name ''{0}'' must match pattern ''{1}''."/>
|
||||
</module>
|
||||
<module name="LambdaParameterName">
|
||||
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
|
||||
|
|
@ -199,38 +246,53 @@
|
|||
<module name="CatchParameterName">
|
||||
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
|
||||
<message key="name.invalidPattern"
|
||||
value="Catch parameter name ''{0}'' must match pattern ''{1}''."/>
|
||||
value="Catch parameter name ''{0}'' must match pattern ''{1}''."/>
|
||||
</module>
|
||||
<module name="LocalVariableName">
|
||||
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
|
||||
<message key="name.invalidPattern"
|
||||
value="Local variable name ''{0}'' must match pattern ''{1}''."/>
|
||||
value="Local variable name ''{0}'' must match pattern ''{1}''."/>
|
||||
</module>
|
||||
<module name="PatternVariableName">
|
||||
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
|
||||
<message key="name.invalidPattern"
|
||||
value="Pattern variable name ''{0}'' must match pattern ''{1}''."/>
|
||||
</module>
|
||||
<module name="ClassTypeParameterName">
|
||||
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
|
||||
<message key="name.invalidPattern"
|
||||
value="Class type name ''{0}'' must match pattern ''{1}''."/>
|
||||
value="Class type name ''{0}'' must match pattern ''{1}''."/>
|
||||
</module>
|
||||
<module name="RecordComponentName">
|
||||
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
|
||||
<message key="name.invalidPattern"
|
||||
value="Record component name ''{0}'' must match pattern ''{1}''."/>
|
||||
</module>
|
||||
<module name="RecordTypeParameterName">
|
||||
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
|
||||
<message key="name.invalidPattern"
|
||||
value="Record type name ''{0}'' must match pattern ''{1}''."/>
|
||||
</module>
|
||||
<module name="MethodTypeParameterName">
|
||||
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
|
||||
<message key="name.invalidPattern"
|
||||
value="Method type name ''{0}'' must match pattern ''{1}''."/>
|
||||
value="Method type name ''{0}'' must match pattern ''{1}''."/>
|
||||
</module>
|
||||
<module name="InterfaceTypeParameterName">
|
||||
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
|
||||
<message key="name.invalidPattern"
|
||||
value="Interface type name ''{0}'' must match pattern ''{1}''."/>
|
||||
value="Interface type name ''{0}'' must match pattern ''{1}''."/>
|
||||
</module>
|
||||
<module name="NoFinalizer"/>
|
||||
<module name="GenericWhitespace">
|
||||
<message key="ws.followed"
|
||||
value="GenericWhitespace ''{0}'' is followed by whitespace."/>
|
||||
value="GenericWhitespace ''{0}'' is followed by whitespace."/>
|
||||
<message key="ws.preceded"
|
||||
value="GenericWhitespace ''{0}'' is preceded with whitespace."/>
|
||||
value="GenericWhitespace ''{0}'' is preceded with whitespace."/>
|
||||
<message key="ws.illegalFollow"
|
||||
value="GenericWhitespace ''{0}'' should followed by whitespace."/>
|
||||
value="GenericWhitespace ''{0}'' should followed by whitespace."/>
|
||||
<message key="ws.notPreceded"
|
||||
value="GenericWhitespace ''{0}'' is not preceded with whitespace."/>
|
||||
value="GenericWhitespace ''{0}'' is not preceded with whitespace."/>
|
||||
</module>
|
||||
<module name="Indentation">
|
||||
<property name="basicOffset" value="4"/>
|
||||
|
|
@ -240,44 +302,62 @@
|
|||
<property name="lineWrappingIndentation" value="4"/>
|
||||
<property name="arrayInitIndent" value="4"/>
|
||||
</module>
|
||||
<!-- Suppression for block code until we find a way to detect them properly
|
||||
until https://github.com/checkstyle/checkstyle/issues/15769 -->
|
||||
<module name="SuppressionXpathSingleFilter">
|
||||
<property name="checks" value="Indentation"/>
|
||||
<property name="query" value="//SLIST[not(parent::CASE_GROUP)]/SLIST
|
||||
| //SLIST[not(parent::CASE_GROUP)]/SLIST/RCURLY"/>
|
||||
</module>
|
||||
<module name="AbbreviationAsWordInName">
|
||||
<property name="ignoreFinal" value="true"/>
|
||||
<property name="allowedAbbreviations" value="API" />
|
||||
<property name="allowedAbbreviationLength" value="1"/>
|
||||
<property name="tokens"
|
||||
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, ANNOTATION_DEF, ANNOTATION_FIELD_DEF,
|
||||
PARAMETER_DEF, VARIABLE_DEF, METHOD_DEF"/>
|
||||
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, ANNOTATION_DEF, ANNOTATION_FIELD_DEF,
|
||||
PARAMETER_DEF, VARIABLE_DEF, METHOD_DEF, PATTERN_VARIABLE_DEF, RECORD_DEF,
|
||||
RECORD_COMPONENT_DEF"/>
|
||||
</module>
|
||||
<module name="NoWhitespaceBeforeCaseDefaultColon"/>
|
||||
<module name="OverloadMethodsDeclarationOrder"/>
|
||||
<!--<module name="ConstructorsDeclarationGrouping"/>-->
|
||||
<module name="VariableDeclarationUsageDistance"/>
|
||||
<module name="CustomImportOrder">
|
||||
<property name="sortImportsInGroupAlphabetically" value="true"/>
|
||||
<property name="separateLineBetweenGroups" value="true"/>
|
||||
<property name="customImportOrderRules" value="STATIC###THIRD_PARTY_PACKAGE"/>
|
||||
<property name="tokens" value="IMPORT, STATIC_IMPORT, PACKAGE_DEF"/>
|
||||
</module>
|
||||
<module name="MethodParamPad">
|
||||
<property name="tokens"
|
||||
value="CTOR_DEF, LITERAL_NEW, METHOD_CALL, METHOD_DEF,
|
||||
value="CTOR_DEF, LITERAL_NEW, METHOD_CALL, METHOD_DEF,
|
||||
SUPER_CTOR_CALL, ENUM_CONSTANT_DEF"/>
|
||||
</module>
|
||||
<module name="NoWhitespaceBefore">
|
||||
<property name="tokens"
|
||||
value="COMMA, SEMI, POST_INC, POST_DEC, DOT, ELLIPSIS, METHOD_REF"/>
|
||||
value="COMMA, SEMI, POST_INC, POST_DEC, DOT,
|
||||
LABELED_STAT, METHOD_REF"/>
|
||||
<property name="allowLineBreaks" value="true"/>
|
||||
</module>
|
||||
<module name="ParenPad">
|
||||
<property name="tokens"
|
||||
value="ANNOTATION, ANNOTATION_FIELD_DEF, CTOR_CALL, CTOR_DEF, DOT, ENUM_CONSTANT_DEF,
|
||||
EXPR, LITERAL_CATCH, LITERAL_DO, LITERAL_FOR, LITERAL_IF, LITERAL_NEW,
|
||||
LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_WHILE, METHOD_CALL,
|
||||
METHOD_DEF, QUESTION, RESOURCE_SPECIFICATION, SUPER_CTOR_CALL, LAMBDA"/>
|
||||
value="ANNOTATION, ANNOTATION_FIELD_DEF, CTOR_DEF, DOT, ENUM_CONSTANT_DEF,
|
||||
EXPR, LITERAL_DO, LITERAL_FOR, LITERAL_IF, LITERAL_NEW,
|
||||
LITERAL_WHILE, METHOD_CALL,
|
||||
METHOD_DEF, QUESTION, RESOURCE_SPECIFICATION, SUPER_CTOR_CALL"/>
|
||||
</module>
|
||||
<module name="OperatorWrap">
|
||||
<property name="option" value="NL"/>
|
||||
<property name="tokens"
|
||||
value="BAND, BOR, BSR, BXOR, DIV, EQUAL, GE, GT, LAND, LE, LITERAL_INSTANCEOF, LOR,
|
||||
LT, MINUS, MOD, NOT_EQUAL, PLUS, QUESTION, SL, SR, STAR, METHOD_REF "/>
|
||||
value="BAND, BOR, BSR, BXOR, DIV, EQUAL, GE, GT, LAND, LE, LITERAL_INSTANCEOF, LOR,
|
||||
LT, MINUS, MOD, NOT_EQUAL, PLUS, QUESTION, SL, SR, STAR, METHOD_REF,
|
||||
TYPE_EXTENSION_AND "/>
|
||||
</module>
|
||||
<module name="AnnotationLocation">
|
||||
<property name="id" value="AnnotationLocationMostCases"/>
|
||||
<property name="tokens"
|
||||
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF"/>
|
||||
<property name="allowSamelineMultipleAnnotations" value="true"/>
|
||||
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF,
|
||||
RECORD_DEF, COMPACT_CTOR_DEF"/>
|
||||
</module>
|
||||
<module name="AnnotationLocation">
|
||||
<property name="id" value="AnnotationLocationVariables"/>
|
||||
|
|
@ -289,46 +369,83 @@
|
|||
<module name="JavadocTagContinuationIndentation"/>
|
||||
<module name="SummaryJavadoc">
|
||||
<property name="forbiddenSummaryFragments"
|
||||
value="^@return the *|^This method returns |^A [{]@code [a-zA-Z0-9]+[}]( is a )"/>
|
||||
value="^@return the *|^This method returns |^A [{]@code [a-zA-Z0-9]+[}]( is a )"/>
|
||||
</module>
|
||||
<module name="JavadocParagraph"/>
|
||||
<module name="JavadocParagraph">
|
||||
</module>
|
||||
<module name="RequireEmptyLineBeforeBlockTagGroup"/>
|
||||
<module name="AtclauseOrder">
|
||||
<property name="tagOrder" value="@param, @return, @throws, @deprecated"/>
|
||||
<property name="target"
|
||||
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF"/>
|
||||
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF"/>
|
||||
</module>
|
||||
<module name="JavadocMethod">
|
||||
<property name="accessModifiers" value="public"/>
|
||||
<property name="allowMissingParamTags" value="true"/>
|
||||
<property name="allowMissingReturnTag" value="true"/>
|
||||
<property name="allowedAnnotations" value="Override, Test"/>
|
||||
<property name="tokens" value="METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF"/>
|
||||
<property name="tokens" value="METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF, COMPACT_CTOR_DEF"/>
|
||||
</module>
|
||||
<module name="MissingJavadocMethod">
|
||||
<property name="scope" value="public"/>
|
||||
<property name="minLineCount" value="2"/>
|
||||
<property name="allowMissingPropertyJavadoc" value="true"/>
|
||||
<property name="allowedAnnotations" value="Override, Test"/>
|
||||
<property name="tokens" value="METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF"/>
|
||||
<property name="tokens" value="METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF,
|
||||
COMPACT_CTOR_DEF"/>
|
||||
</module>
|
||||
<module name="SuppressionXpathSingleFilter">
|
||||
<property name="checks" value="MissingJavadocMethod"/>
|
||||
<property name="query" value="//*[self::METHOD_DEF or self::CTOR_DEF
|
||||
or self::ANNOTATION_FIELD_DEF or self::COMPACT_CTOR_DEF]
|
||||
[ancestor::*[self::INTERFACE_DEF or self::CLASS_DEF
|
||||
or self::RECORD_DEF or self::ENUM_DEF]
|
||||
[not(./MODIFIERS/LITERAL_PUBLIC)]]"/>
|
||||
</module>
|
||||
<module name="MissingJavadocType">
|
||||
<property name="scope" value="protected"/>
|
||||
<property name="tokens" value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF,
|
||||
<property name="tokens"
|
||||
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF,
|
||||
RECORD_DEF, ANNOTATION_DEF"/>
|
||||
<property name="excludeScope" value="nothing"/>
|
||||
</module>
|
||||
<module name="MethodName">
|
||||
<property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9_]*$"/>
|
||||
<property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9]*$"/>
|
||||
<message key="name.invalidPattern"
|
||||
value="Method name ''{0}'' must match pattern ''{1}''."/>
|
||||
value="Method name ''{0}'' must match pattern ''{1}''."/>
|
||||
</module>
|
||||
<module name="SingleLineJavadoc">
|
||||
<property name="ignoreInlineTags" value="false"/>
|
||||
<module name="SuppressionXpathSingleFilter">
|
||||
<property name="checks" value="MethodName"/>
|
||||
<property name="query" value="//METHOD_DEF[
|
||||
./MODIFIERS/ANNOTATION//IDENT[contains(@text, 'Test')]
|
||||
]/IDENT"/>
|
||||
<property name="message" value="'[a-z][a-z0-9][a-zA-Z0-9]*(?:_[a-z][a-z0-9][a-zA-Z0-9]*)*'"/>
|
||||
</module>
|
||||
<module name="SingleLineJavadoc"/>
|
||||
<module name="EmptyCatchBlock">
|
||||
<property name="exceptionVariableName" value="expected"/>
|
||||
</module>
|
||||
<module name="CommentsIndentation">
|
||||
<property name="tokens" value="SINGLE_LINE_COMMENT, BLOCK_COMMENT_BEGIN"/>
|
||||
</module>
|
||||
<!-- https://checkstyle.org/filters/suppressionxpathfilter.html -->
|
||||
<module name="SuppressionXpathFilter">
|
||||
<property name="file" value="${org.checkstyle.google.suppressionxpathfilter.config}"
|
||||
default="checkstyle-xpath-suppressions.xml" />
|
||||
<property name="optional" value="true"/>
|
||||
</module>
|
||||
<module name="SuppressWarningsHolder" />
|
||||
<module name="SuppressionCommentFilter">
|
||||
<property name="offCommentFormat" value="CHECKSTYLE.OFF\: ([\w\|]+)" />
|
||||
<property name="onCommentFormat" value="CHECKSTYLE.ON\: ([\w\|]+)" />
|
||||
<property name="checkFormat" value="$1" />
|
||||
</module>
|
||||
<module name="SuppressWithNearbyCommentFilter">
|
||||
<property name="commentFormat" value="CHECKSTYLE.SUPPRESS\: ([\w\|]+)"/>
|
||||
<!-- $1 refers to the first match group in the regex defined in commentFormat -->
|
||||
<property name="checkFormat" value="$1"/>
|
||||
<!-- The check is suppressed in the next line of code after the comment -->
|
||||
<property name="influenceFormat" value="1"/>
|
||||
</module>
|
||||
</module>
|
||||
</module>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Collections;
|
||||
|
||||
@SuppressWarnings({ "PMD.BeanMembersShouldSerialize", "checkstyle:MissingJavadocType" })
|
||||
@SuppressWarnings({"PMD.BeanMembersShouldSerialize", "checkstyle:MissingJavadocType"})
|
||||
abstract class AbstractStructure implements Structure {
|
||||
|
||||
protected final Map<String, Value> attributes;
|
||||
|
|
@ -24,6 +24,7 @@ abstract class AbstractStructure implements Structure {
|
|||
|
||||
/**
|
||||
* Returns an unmodifiable representation of the internal attribute map.
|
||||
*
|
||||
* @return immutable map
|
||||
*/
|
||||
public Map<String, Value> asUnmodifiableMap() {
|
||||
|
|
@ -37,14 +38,12 @@ abstract class AbstractStructure implements Structure {
|
|||
*/
|
||||
@Override
|
||||
public Map<String, Object> asObjectMap() {
|
||||
return attributes
|
||||
.entrySet()
|
||||
.stream()
|
||||
return attributes.entrySet().stream()
|
||||
// custom collector, workaround for Collectors.toMap in JDK8
|
||||
// https://bugs.openjdk.org/browse/JDK-8148463
|
||||
.collect(HashMap::new,
|
||||
.collect(
|
||||
HashMap::new,
|
||||
(accumulated, entry) -> accumulated.put(entry.getKey(), convertValue(entry.getValue())),
|
||||
HashMap::putAll);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,29 +2,34 @@ package dev.openfeature.sdk;
|
|||
|
||||
/**
|
||||
* This is a common interface between the evaluation results that providers return and what is given to the end users.
|
||||
*
|
||||
* @param <T> The type of flag being evaluated.
|
||||
*/
|
||||
public interface BaseEvaluation<T> {
|
||||
/**
|
||||
* Returns the resolved value of the evaluation.
|
||||
*
|
||||
* @return {T} the resolve value
|
||||
*/
|
||||
T getValue();
|
||||
|
||||
/**
|
||||
* Returns an identifier for this value, if applicable.
|
||||
*
|
||||
* @return {String} value identifier
|
||||
*/
|
||||
String getVariant();
|
||||
|
||||
/**
|
||||
* Describes how we came to the value that we're returning.
|
||||
*
|
||||
* @return {Reason}
|
||||
*/
|
||||
String getReason();
|
||||
|
||||
/**
|
||||
* The error code, if applicable. Should only be set when the Reason is ERROR.
|
||||
*
|
||||
* @return {ErrorCode}
|
||||
*/
|
||||
ErrorCode getErrorCode();
|
||||
|
|
@ -32,6 +37,7 @@ public interface BaseEvaluation<T> {
|
|||
/**
|
||||
* The error message (usually from exception.getMessage()), if applicable.
|
||||
* Should only be set when the Reason is ERROR.
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
String getErrorMessage();
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package dev.openfeature.sdk;
|
|||
/**
|
||||
* An extension point which can run around flag resolution. They are intended to be used as a way to add custom logic
|
||||
* to the lifecycle of flag evaluation.
|
||||
*
|
||||
*
|
||||
* @see Hook
|
||||
*/
|
||||
public interface BooleanHook extends Hook<Boolean> {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package dev.openfeature.sdk;
|
|||
/**
|
||||
* An extension point which can run around flag resolution. They are intended to be used as a way to add custom logic
|
||||
* to the lifecycle of flag evaluation.
|
||||
*
|
||||
*
|
||||
* @see Hook
|
||||
*/
|
||||
public interface DoubleHook extends Hook<Double> {
|
||||
|
|
@ -12,4 +12,4 @@ public interface DoubleHook extends Hook<Double> {
|
|||
default boolean supportsFlagValueType(FlagValueType flagValueType) {
|
||||
return FlagValueType.DOUBLE == flagValueType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,12 @@ package dev.openfeature.sdk;
|
|||
|
||||
@SuppressWarnings("checkstyle:MissingJavadocType")
|
||||
public enum ErrorCode {
|
||||
PROVIDER_NOT_READY, FLAG_NOT_FOUND, PARSE_ERROR, TYPE_MISMATCH, TARGETING_KEY_MISSING, INVALID_CONTEXT, GENERAL,
|
||||
PROVIDER_NOT_READY,
|
||||
FLAG_NOT_FOUND,
|
||||
PARSE_ERROR,
|
||||
TYPE_MISMATCH,
|
||||
TARGETING_KEY_MISSING,
|
||||
INVALID_CONTEXT,
|
||||
GENERAL,
|
||||
PROVIDER_FATAL
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,12 +28,13 @@ public interface EvaluationContext extends Structure {
|
|||
* Recursively merges the overriding map into the base Value map.
|
||||
* The base map is mutated, the overriding map is not.
|
||||
* Null maps will cause no-op.
|
||||
*
|
||||
*
|
||||
* @param newStructure function to create the right structure(s) for Values
|
||||
* @param base base map to merge
|
||||
* @param overriding overriding map to merge
|
||||
* @param base base map to merge
|
||||
* @param overriding overriding map to merge
|
||||
*/
|
||||
static void mergeMaps(Function<Map<String, Value>, Structure> newStructure,
|
||||
static void mergeMaps(
|
||||
Function<Map<String, Value>, Structure> newStructure,
|
||||
Map<String, Value> base,
|
||||
Map<String, Value> overriding) {
|
||||
|
||||
|
|
@ -46,12 +47,13 @@ public interface EvaluationContext extends Structure {
|
|||
|
||||
for (Entry<String, Value> overridingEntry : overriding.entrySet()) {
|
||||
String key = overridingEntry.getKey();
|
||||
if (overridingEntry.getValue().isStructure() && base.containsKey(key) && base.get(key).isStructure()) {
|
||||
if (overridingEntry.getValue().isStructure()
|
||||
&& base.containsKey(key)
|
||||
&& base.get(key).isStructure()) {
|
||||
Structure mergedValue = base.get(key).asStructure();
|
||||
Structure overridingValue = overridingEntry.getValue().asStructure();
|
||||
Map<String, Value> newMap = mergedValue.asMap();
|
||||
mergeMaps(newStructure, newMap,
|
||||
overridingValue.asUnmodifiableMap());
|
||||
mergeMaps(newStructure, newMap, overridingValue.asUnmodifiableMap());
|
||||
base.put(key, new Value(newStructure.apply(newMap)));
|
||||
} else {
|
||||
base.put(key, overridingEntry.getValue());
|
||||
|
|
|
|||
|
|
@ -6,38 +6,38 @@ import java.util.function.Consumer;
|
|||
* Interface for attaching event handlers.
|
||||
*/
|
||||
public interface EventBus<T> {
|
||||
|
||||
|
||||
/**
|
||||
* Add a handler for the {@link ProviderEvent#PROVIDER_READY} event.
|
||||
* Shorthand for {@link #on(ProviderEvent, Consumer)}
|
||||
*
|
||||
*
|
||||
* @param handler behavior to add with this event
|
||||
* @return this
|
||||
*/
|
||||
T onProviderReady(Consumer<EventDetails> handler);
|
||||
|
||||
|
||||
/**
|
||||
* Add a handler for the {@link ProviderEvent#PROVIDER_CONFIGURATION_CHANGED} event.
|
||||
* Shorthand for {@link #on(ProviderEvent, Consumer)}
|
||||
*
|
||||
*
|
||||
* @param handler behavior to add with this event
|
||||
* @return this
|
||||
*/
|
||||
T onProviderConfigurationChanged(Consumer<EventDetails> handler);
|
||||
|
||||
|
||||
/**
|
||||
* Add a handler for the {@link ProviderEvent#PROVIDER_STALE} event.
|
||||
* Shorthand for {@link #on(ProviderEvent, Consumer)}
|
||||
*
|
||||
*
|
||||
* @param handler behavior to add with this event
|
||||
* @return this
|
||||
*/
|
||||
T onProviderError(Consumer<EventDetails> handler);
|
||||
|
||||
|
||||
/**
|
||||
* Add a handler for the {@link ProviderEvent#PROVIDER_ERROR} event.
|
||||
* Shorthand for {@link #on(ProviderEvent, Consumer)}
|
||||
*
|
||||
*
|
||||
* @param handler behavior to add with this event
|
||||
* @return this
|
||||
*/
|
||||
|
|
@ -45,18 +45,18 @@ public interface EventBus<T> {
|
|||
|
||||
/**
|
||||
* Add a handler for the specified {@link ProviderEvent}.
|
||||
*
|
||||
* @param event event type
|
||||
*
|
||||
* @param event event type
|
||||
* @param handler behavior to add with this event
|
||||
* @return this
|
||||
*/
|
||||
T on(ProviderEvent event, Consumer<EventDetails> handler);
|
||||
|
||||
|
||||
/**
|
||||
* Remove the previously attached handler by reference.
|
||||
* If the handler doesn't exists, no-op.
|
||||
*
|
||||
* @param event event type
|
||||
*
|
||||
* @param event event type
|
||||
* @param handler to be removed
|
||||
* @return this
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -17,9 +17,7 @@ public class EventDetails extends ProviderEventDetails {
|
|||
}
|
||||
|
||||
static EventDetails fromProviderEventDetails(
|
||||
ProviderEventDetails providerEventDetails,
|
||||
String providerName,
|
||||
String domain) {
|
||||
ProviderEventDetails providerEventDetails, String providerName, String domain) {
|
||||
return builder()
|
||||
.domain(domain)
|
||||
.providerName(providerName)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package dev.openfeature.sdk;
|
|||
|
||||
import dev.openfeature.sdk.internal.TriConsumer;
|
||||
|
||||
|
||||
/**
|
||||
* Abstract EventProvider. Providers must extend this class to support events.
|
||||
* Emit events with {@link #emit(ProviderEvent, ProviderEventDetails)}. Please
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
|
@ -13,6 +11,7 @@ import java.util.concurrent.ExecutorService;
|
|||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Util class for storing and running handlers.
|
||||
|
|
@ -35,74 +34,68 @@ class EventSupport {
|
|||
/**
|
||||
* Run all the event handlers associated with this domain.
|
||||
* If the domain is null, handlers attached to unnamed clients will run.
|
||||
*
|
||||
*
|
||||
* @param domain the domain to run event handlers for, or null
|
||||
* @param event the event type
|
||||
* @param eventDetails the event details
|
||||
*/
|
||||
public void runClientHandlers(String domain, ProviderEvent event, EventDetails eventDetails) {
|
||||
domain = Optional.ofNullable(domain)
|
||||
.orElse(defaultClientUuid);
|
||||
domain = Optional.ofNullable(domain).orElse(defaultClientUuid);
|
||||
|
||||
// run handlers if they exist
|
||||
Optional.ofNullable(handlerStores.get(domain))
|
||||
.filter(store -> Optional.of(store).isPresent())
|
||||
.map(store -> store.handlerMap.get(event))
|
||||
.ifPresent(handlers -> handlers
|
||||
.forEach(handler -> runHandler(handler, eventDetails)));
|
||||
.ifPresent(handlers -> handlers.forEach(handler -> runHandler(handler, eventDetails)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Run all the API (global) event handlers.
|
||||
*
|
||||
*
|
||||
* @param event the event type
|
||||
* @param eventDetails the event details
|
||||
*/
|
||||
public void runGlobalHandlers(ProviderEvent event, EventDetails eventDetails) {
|
||||
globalHandlerStore.handlerMap.get(event)
|
||||
.forEach(handler -> {
|
||||
runHandler(handler, eventDetails);
|
||||
});
|
||||
globalHandlerStore.handlerMap.get(event).forEach(handler -> {
|
||||
runHandler(handler, eventDetails);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a handler for the specified domain, or all unnamed clients.
|
||||
*
|
||||
* @param domain the domain to add handlers for, or else unnamed
|
||||
* @param event the event type
|
||||
* @param handler the handler function to run
|
||||
*
|
||||
* @param domain the domain to add handlers for, or else unnamed
|
||||
* @param event the event type
|
||||
* @param handler the handler function to run
|
||||
*/
|
||||
public void addClientHandler(String domain, ProviderEvent event, Consumer<EventDetails> handler) {
|
||||
final String name = Optional.ofNullable(domain)
|
||||
.orElse(defaultClientUuid);
|
||||
final String name = Optional.ofNullable(domain).orElse(defaultClientUuid);
|
||||
|
||||
// lazily create and cache a HandlerStore if it doesn't exist
|
||||
HandlerStore store = Optional.ofNullable(this.handlerStores.get(name))
|
||||
.orElseGet(() -> {
|
||||
HandlerStore newStore = new HandlerStore();
|
||||
this.handlerStores.put(name, newStore);
|
||||
return newStore;
|
||||
});
|
||||
HandlerStore store = Optional.ofNullable(this.handlerStores.get(name)).orElseGet(() -> {
|
||||
HandlerStore newStore = new HandlerStore();
|
||||
this.handlerStores.put(name, newStore);
|
||||
return newStore;
|
||||
});
|
||||
store.addHandler(event, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a client event handler for the specified event type.
|
||||
*
|
||||
* @param domain the domain of the client handler to remove, or null to remove
|
||||
* from unnamed clients
|
||||
* @param event the event type
|
||||
* @param handler the handler ref to be removed
|
||||
*
|
||||
* @param domain the domain of the client handler to remove, or null to remove
|
||||
* from unnamed clients
|
||||
* @param event the event type
|
||||
* @param handler the handler ref to be removed
|
||||
*/
|
||||
public void removeClientHandler(String domain, ProviderEvent event, Consumer<EventDetails> handler) {
|
||||
domain = Optional.ofNullable(domain)
|
||||
.orElse(defaultClientUuid);
|
||||
domain = Optional.ofNullable(domain).orElse(defaultClientUuid);
|
||||
this.handlerStores.get(domain).removeHandler(event, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a global event handler of the specified event type.
|
||||
*
|
||||
*
|
||||
* @param event the event type
|
||||
* @param handler the handler to be added
|
||||
*/
|
||||
|
|
@ -112,7 +105,7 @@ class EventSupport {
|
|||
|
||||
/**
|
||||
* Remove a global event handler for the specified event type.
|
||||
*
|
||||
*
|
||||
* @param event the event type
|
||||
* @param handler the handler ref to be removed
|
||||
*/
|
||||
|
|
@ -122,7 +115,7 @@ class EventSupport {
|
|||
|
||||
/**
|
||||
* Get all domain names for which we have event handlers registered.
|
||||
*
|
||||
*
|
||||
* @return set of domain names
|
||||
*/
|
||||
public Set<String> getAllDomainNames() {
|
||||
|
|
@ -131,7 +124,7 @@ class EventSupport {
|
|||
|
||||
/**
|
||||
* Run the passed handler on the taskExecutor.
|
||||
*
|
||||
*
|
||||
* @param handler the handler to run
|
||||
* @param eventDetails the event details
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -78,7 +78,5 @@ public interface FeatureProvider {
|
|||
* @param context Evaluation context used in flag evaluation (Optional)
|
||||
* @param details Data pertinent to a particular tracking event (Optional)
|
||||
*/
|
||||
default void track(String eventName, EvaluationContext context, TrackingEventDetails details) {
|
||||
|
||||
}
|
||||
default void track(String eventName, EvaluationContext context, TrackingEventDetails details) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import dev.openfeature.sdk.exceptions.OpenFeatureError;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import lombok.Getter;
|
||||
|
||||
class FeatureProviderStateManager implements EventProviderListener {
|
||||
private final FeatureProvider delegate;
|
||||
private final AtomicBoolean isInitialized = new AtomicBoolean();
|
||||
|
||||
@Getter
|
||||
private ProviderState state = ProviderState.NOT_READY;
|
||||
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ public interface Features {
|
|||
|
||||
FlagEvaluationDetails<Boolean> getBooleanDetails(String key, Boolean defaultValue, EvaluationContext ctx);
|
||||
|
||||
FlagEvaluationDetails<Boolean> getBooleanDetails(String key, Boolean defaultValue, EvaluationContext ctx,
|
||||
FlagEvaluationOptions options);
|
||||
FlagEvaluationDetails<Boolean> getBooleanDetails(
|
||||
String key, Boolean defaultValue, EvaluationContext ctx, FlagEvaluationOptions options);
|
||||
|
||||
String getStringValue(String key, String defaultValue);
|
||||
|
||||
|
|
@ -28,8 +28,8 @@ public interface Features {
|
|||
|
||||
FlagEvaluationDetails<String> getStringDetails(String key, String defaultValue, EvaluationContext ctx);
|
||||
|
||||
FlagEvaluationDetails<String> getStringDetails(String key, String defaultValue, EvaluationContext ctx,
|
||||
FlagEvaluationOptions options);
|
||||
FlagEvaluationDetails<String> getStringDetails(
|
||||
String key, String defaultValue, EvaluationContext ctx, FlagEvaluationOptions options);
|
||||
|
||||
Integer getIntegerValue(String key, Integer defaultValue);
|
||||
|
||||
|
|
@ -41,8 +41,8 @@ public interface Features {
|
|||
|
||||
FlagEvaluationDetails<Integer> getIntegerDetails(String key, Integer defaultValue, EvaluationContext ctx);
|
||||
|
||||
FlagEvaluationDetails<Integer> getIntegerDetails(String key, Integer defaultValue, EvaluationContext ctx,
|
||||
FlagEvaluationOptions options);
|
||||
FlagEvaluationDetails<Integer> getIntegerDetails(
|
||||
String key, Integer defaultValue, EvaluationContext ctx, FlagEvaluationOptions options);
|
||||
|
||||
Double getDoubleValue(String key, Double defaultValue);
|
||||
|
||||
|
|
@ -54,22 +54,19 @@ public interface Features {
|
|||
|
||||
FlagEvaluationDetails<Double> getDoubleDetails(String key, Double defaultValue, EvaluationContext ctx);
|
||||
|
||||
FlagEvaluationDetails<Double> getDoubleDetails(String key, Double defaultValue, EvaluationContext ctx,
|
||||
FlagEvaluationOptions options);
|
||||
FlagEvaluationDetails<Double> getDoubleDetails(
|
||||
String key, Double defaultValue, EvaluationContext ctx, FlagEvaluationOptions options);
|
||||
|
||||
Value getObjectValue(String key, Value defaultValue);
|
||||
|
||||
Value getObjectValue(String key, Value defaultValue, EvaluationContext ctx);
|
||||
|
||||
Value getObjectValue(String key, Value defaultValue, EvaluationContext ctx,
|
||||
FlagEvaluationOptions options);
|
||||
Value getObjectValue(String key, Value defaultValue, EvaluationContext ctx, FlagEvaluationOptions options);
|
||||
|
||||
FlagEvaluationDetails<Value> getObjectDetails(String key, Value defaultValue);
|
||||
|
||||
FlagEvaluationDetails<Value> getObjectDetails(String key, Value defaultValue,
|
||||
EvaluationContext ctx);
|
||||
FlagEvaluationDetails<Value> getObjectDetails(String key, Value defaultValue, EvaluationContext ctx);
|
||||
|
||||
FlagEvaluationDetails<Value> getObjectDetails(String key, Value defaultValue,
|
||||
EvaluationContext ctx,
|
||||
FlagEvaluationOptions options);
|
||||
FlagEvaluationDetails<Value> getObjectDetails(
|
||||
String key, Value defaultValue, EvaluationContext ctx, FlagEvaluationOptions options);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
|
@ -25,6 +24,7 @@ public class FlagEvaluationDetails<T> implements BaseEvaluation<T> {
|
|||
private String reason;
|
||||
private ErrorCode errorCode;
|
||||
private String errorMessage;
|
||||
|
||||
@Builder.Default
|
||||
private ImmutableMetadata flagMetadata = ImmutableMetadata.builder().build();
|
||||
|
||||
|
|
@ -44,8 +44,8 @@ public class FlagEvaluationDetails<T> implements BaseEvaluation<T> {
|
|||
.reason(providerEval.getReason())
|
||||
.errorMessage(providerEval.getErrorMessage())
|
||||
.errorCode(providerEval.getErrorCode())
|
||||
.flagMetadata(
|
||||
Optional.ofNullable(providerEval.getFlagMetadata()).orElse(ImmutableMetadata.builder().build()))
|
||||
.flagMetadata(Optional.ofNullable(providerEval.getFlagMetadata())
|
||||
.orElse(ImmutableMetadata.builder().build()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package dev.openfeature.sdk;
|
|||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Singular;
|
||||
|
||||
|
|
@ -13,6 +12,7 @@ import lombok.Singular;
|
|||
public class FlagEvaluationOptions {
|
||||
@Singular
|
||||
List<Hook> hooks;
|
||||
|
||||
@Builder.Default
|
||||
Map<String, Object> hookHints = new HashMap<>();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,5 +2,9 @@ package dev.openfeature.sdk;
|
|||
|
||||
@SuppressWarnings("checkstyle:MissingJavadocType")
|
||||
public enum FlagValueType {
|
||||
STRING, INTEGER, DOUBLE, OBJECT, BOOLEAN;
|
||||
STRING,
|
||||
INTEGER,
|
||||
DOUBLE,
|
||||
OBJECT,
|
||||
BOOLEAN;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ public interface Hook<T> {
|
|||
* @param ctx Information about the particular flag evaluation
|
||||
* @param hints An immutable mapping of data for users to communicate to the hooks.
|
||||
* @return An optional {@link EvaluationContext}. If returned, it will be merged with the EvaluationContext
|
||||
* instances from other hooks, the client and API.
|
||||
* instances from other hooks, the client and API.
|
||||
*/
|
||||
default Optional<EvaluationContext> before(HookContext<T> ctx, Map<String, Object> hints) {
|
||||
return Optional.empty();
|
||||
|
|
@ -29,8 +29,7 @@ public interface Hook<T> {
|
|||
* @param details Information about how the flag was resolved, including any resolved values.
|
||||
* @param hints An immutable mapping of data for users to communicate to the hooks.
|
||||
*/
|
||||
default void after(HookContext<T> ctx, FlagEvaluationDetails<T> details, Map<String, Object> hints) {
|
||||
}
|
||||
default void after(HookContext<T> ctx, FlagEvaluationDetails<T> details, Map<String, Object> hints) {}
|
||||
|
||||
/**
|
||||
* Run when evaluation encounters an error. This will always run. Errors thrown will be swallowed.
|
||||
|
|
@ -39,8 +38,7 @@ public interface Hook<T> {
|
|||
* @param error The exception that was thrown.
|
||||
* @param hints An immutable mapping of data for users to communicate to the hooks.
|
||||
*/
|
||||
default void error(HookContext<T> ctx, Exception error, Map<String, Object> hints) {
|
||||
}
|
||||
default void error(HookContext<T> ctx, Exception error, Map<String, Object> hints) {}
|
||||
|
||||
/**
|
||||
* Run after flag evaluation, including any error processing. This will always run. Errors will be swallowed.
|
||||
|
|
@ -48,8 +46,7 @@ public interface Hook<T> {
|
|||
* @param ctx Information about the particular flag evaluation
|
||||
* @param hints An immutable mapping of data for users to communicate to the hooks.
|
||||
*/
|
||||
default void finallyAfter(HookContext<T> ctx, Map<String, Object> hints) {
|
||||
}
|
||||
default void finallyAfter(HookContext<T> ctx, Map<String, Object> hints) {}
|
||||
|
||||
default boolean supportsFlagValueType(FlagValueType flagValueType) {
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -10,28 +10,40 @@ import lombok.With;
|
|||
*
|
||||
* @param <T> the type for the flag being evaluated
|
||||
*/
|
||||
@Value @Builder @With
|
||||
@Value
|
||||
@Builder
|
||||
@With
|
||||
public class HookContext<T> {
|
||||
@NonNull String flagKey;
|
||||
|
||||
@NonNull FlagValueType type;
|
||||
|
||||
@NonNull T defaultValue;
|
||||
|
||||
@NonNull EvaluationContext ctx;
|
||||
|
||||
ClientMetadata clientMetadata;
|
||||
Metadata providerMetadata;
|
||||
|
||||
/**
|
||||
* Builds a {@link HookContext} instances from request data.
|
||||
* @param key feature flag key
|
||||
* @param type flag value type
|
||||
* @param clientMetadata info on which client is calling
|
||||
*
|
||||
* @param key feature flag key
|
||||
* @param type flag value type
|
||||
* @param clientMetadata info on which client is calling
|
||||
* @param providerMetadata info on the provider
|
||||
* @param ctx Evaluation Context for the request
|
||||
* @param defaultValue Fallback value
|
||||
* @param <T> type that the flag is evaluating against
|
||||
* @param ctx Evaluation Context for the request
|
||||
* @param defaultValue Fallback value
|
||||
* @param <T> type that the flag is evaluating against
|
||||
* @return resulting context for hook
|
||||
*/
|
||||
public static <T> HookContext<T> from(String key, FlagValueType type, ClientMetadata clientMetadata,
|
||||
Metadata providerMetadata, EvaluationContext ctx, T defaultValue) {
|
||||
public static <T> HookContext<T> from(
|
||||
String key,
|
||||
FlagValueType type,
|
||||
ClientMetadata clientMetadata,
|
||||
Metadata providerMetadata,
|
||||
EvaluationContext ctx,
|
||||
T defaultValue) {
|
||||
return HookContext.<T>builder()
|
||||
.flagKey(key)
|
||||
.type(type)
|
||||
|
|
|
|||
|
|
@ -6,39 +6,44 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
class HookSupport {
|
||||
|
||||
public EvaluationContext beforeHooks(FlagValueType flagValueType, HookContext hookCtx, List<Hook> hooks,
|
||||
Map<String, Object> hints) {
|
||||
public EvaluationContext beforeHooks(
|
||||
FlagValueType flagValueType, HookContext hookCtx, List<Hook> hooks, Map<String, Object> hints) {
|
||||
return callBeforeHooks(flagValueType, hookCtx, hooks, hints);
|
||||
}
|
||||
|
||||
public void afterHooks(FlagValueType flagValueType, HookContext hookContext, FlagEvaluationDetails details,
|
||||
List<Hook> hooks, Map<String, Object> hints) {
|
||||
public void afterHooks(
|
||||
FlagValueType flagValueType,
|
||||
HookContext hookContext,
|
||||
FlagEvaluationDetails details,
|
||||
List<Hook> hooks,
|
||||
Map<String, Object> hints) {
|
||||
executeHooksUnchecked(flagValueType, hooks, hook -> hook.after(hookContext, details, hints));
|
||||
}
|
||||
|
||||
public void afterAllHooks(FlagValueType flagValueType, HookContext hookCtx, List<Hook> hooks,
|
||||
Map<String, Object> hints) {
|
||||
public void afterAllHooks(
|
||||
FlagValueType flagValueType, HookContext hookCtx, List<Hook> hooks, Map<String, Object> hints) {
|
||||
executeHooks(flagValueType, hooks, "finally", hook -> hook.finallyAfter(hookCtx, hints));
|
||||
}
|
||||
|
||||
public void errorHooks(FlagValueType flagValueType, HookContext hookCtx, Exception e, List<Hook> hooks,
|
||||
public void errorHooks(
|
||||
FlagValueType flagValueType,
|
||||
HookContext hookCtx,
|
||||
Exception e,
|
||||
List<Hook> hooks,
|
||||
Map<String, Object> hints) {
|
||||
executeHooks(flagValueType, hooks, "error", hook -> hook.error(hookCtx, e, hints));
|
||||
}
|
||||
|
||||
private <T> void executeHooks(
|
||||
FlagValueType flagValueType, List<Hook> hooks,
|
||||
String hookMethod,
|
||||
Consumer<Hook<T>> hookCode) {
|
||||
FlagValueType flagValueType, List<Hook> hooks, String hookMethod, Consumer<Hook<T>> hookCode) {
|
||||
if (hooks != null) {
|
||||
for (Hook hook : hooks) {
|
||||
if (hook.supportsFlagValueType(flagValueType)) {
|
||||
|
|
@ -53,15 +58,16 @@ class HookSupport {
|
|||
try {
|
||||
hookCode.accept(hook);
|
||||
} catch (Exception exception) {
|
||||
log.error("Unhandled exception when running {} hook {} (only 'after' hooks should throw)", hookMethod,
|
||||
hook.getClass(), exception);
|
||||
log.error(
|
||||
"Unhandled exception when running {} hook {} (only 'after' hooks should throw)",
|
||||
hookMethod,
|
||||
hook.getClass(),
|
||||
exception);
|
||||
}
|
||||
}
|
||||
|
||||
// after hooks can throw in order to do validation
|
||||
private <T> void executeHooksUnchecked(
|
||||
FlagValueType flagValueType, List<Hook> hooks,
|
||||
Consumer<Hook<T>> hookCode) {
|
||||
private <T> void executeHooksUnchecked(FlagValueType flagValueType, List<Hook> hooks, Consumer<Hook<T>> hookCode) {
|
||||
if (hooks != null) {
|
||||
for (Hook hook : hooks) {
|
||||
if (hook.supportsFlagValueType(flagValueType)) {
|
||||
|
|
@ -71,16 +77,16 @@ class HookSupport {
|
|||
}
|
||||
}
|
||||
|
||||
private EvaluationContext callBeforeHooks(FlagValueType flagValueType, HookContext hookCtx,
|
||||
List<Hook> hooks, Map<String, Object> hints) {
|
||||
private EvaluationContext callBeforeHooks(
|
||||
FlagValueType flagValueType, HookContext hookCtx, List<Hook> hooks, Map<String, Object> hints) {
|
||||
// These traverse backwards from normal.
|
||||
List<Hook> reversedHooks = new ArrayList<>(hooks);
|
||||
Collections.reverse(reversedHooks);
|
||||
EvaluationContext context = hookCtx.getCtx();
|
||||
for (Hook hook : reversedHooks) {
|
||||
if (hook.supportsFlagValueType(flagValueType)) {
|
||||
Optional<EvaluationContext> optional = Optional.ofNullable(hook.before(hookCtx, hints))
|
||||
.orElse(Optional.empty());
|
||||
Optional<EvaluationContext> optional =
|
||||
Optional.ofNullable(hook.before(hookCtx, hints)).orElse(Optional.empty());
|
||||
if (optional.isPresent()) {
|
||||
context = context.merge(optional.get());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import dev.openfeature.sdk.internal.ExcludeFromGeneratedCoverageReport;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import dev.openfeature.sdk.internal.ExcludeFromGeneratedCoverageReport;
|
||||
import lombok.ToString;
|
||||
import lombok.experimental.Delegate;
|
||||
|
||||
|
|
@ -88,15 +87,15 @@ public final class ImmutableContext implements EvaluationContext {
|
|||
}
|
||||
|
||||
Map<String, Value> attributes = this.asMap();
|
||||
EvaluationContext.mergeMaps(ImmutableStructure::new, attributes,
|
||||
overridingContext.asUnmodifiableMap());
|
||||
EvaluationContext.mergeMaps(ImmutableStructure::new, attributes, overridingContext.asUnmodifiableMap());
|
||||
return new ImmutableContext(attributes);
|
||||
}
|
||||
|
||||
@SuppressWarnings("all")
|
||||
private static class DelegateExclusions {
|
||||
@ExcludeFromGeneratedCoverageReport
|
||||
public <T extends Structure> Map<String, Value> merge(Function<Map<String, Value>, Structure> newStructure,
|
||||
public <T extends Structure> Map<String, Value> merge(
|
||||
Function<Map<String, Value>, Structure> newStructure,
|
||||
Map<String, Value> base,
|
||||
Map<String, Value> overriding) {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Immutable Flag Metadata representation. Implementation is backed by a {@link Map} and immutability is provided
|
||||
|
|
@ -98,7 +97,6 @@ public class ImmutableMetadata {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Obtain a builder for {@link ImmutableMetadata}.
|
||||
*/
|
||||
|
|
@ -188,6 +186,5 @@ public class ImmutableMetadata {
|
|||
public ImmutableMetadata build() {
|
||||
return new ImmutableMetadata(this.metadata);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import java.util.Map;
|
|||
import java.util.Map.Entry;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
|
|
@ -20,7 +19,7 @@ import lombok.ToString;
|
|||
*/
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
@SuppressWarnings({ "PMD.BeanMembersShouldSerialize", "checkstyle:MissingJavadocType" })
|
||||
@SuppressWarnings({"PMD.BeanMembersShouldSerialize", "checkstyle:MissingJavadocType"})
|
||||
public final class ImmutableStructure extends AbstractStructure {
|
||||
|
||||
/**
|
||||
|
|
@ -72,13 +71,15 @@ public final class ImmutableStructure extends AbstractStructure {
|
|||
private static Map<String, Value> copyAttributes(Map<String, Value> in, String targetingKey) {
|
||||
Map<String, Value> copy = new HashMap<>();
|
||||
for (Entry<String, Value> entry : in.entrySet()) {
|
||||
copy.put(entry.getKey(),
|
||||
Optional.ofNullable(entry.getValue()).map((Value val) -> val.clone()).orElse(null));
|
||||
copy.put(
|
||||
entry.getKey(),
|
||||
Optional.ofNullable(entry.getValue())
|
||||
.map((Value val) -> val.clone())
|
||||
.orElse(null));
|
||||
}
|
||||
if (targetingKey != null) {
|
||||
copy.put(EvaluationContext.TARGETING_KEY, new Value(targetingKey));
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import dev.openfeature.sdk.internal.ExcludeFromGeneratedCoverageReport;
|
||||
import lombok.experimental.Delegate;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
|
||||
import lombok.experimental.Delegate;
|
||||
|
||||
/**
|
||||
* ImmutableTrackingEventDetails represents data pertinent to a particular tracking event.
|
||||
|
|
@ -40,13 +38,13 @@ public class ImmutableTrackingEventDetails implements TrackingEventDetails {
|
|||
return Optional.ofNullable(value);
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("all")
|
||||
private static class DelegateExclusions {
|
||||
@ExcludeFromGeneratedCoverageReport
|
||||
public <T extends Structure> Map<String, Value> merge(Function<Map<String, Value>, Structure> newStructure,
|
||||
Map<String, Value> base,
|
||||
Map<String, Value> overriding) {
|
||||
public <T extends Structure> Map<String, Value> merge(
|
||||
Function<Map<String, Value>, Structure> newStructure,
|
||||
Map<String, Value> base,
|
||||
Map<String, Value> overriding) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package dev.openfeature.sdk;
|
|||
/**
|
||||
* An extension point which can run around flag resolution. They are intended to be used as a way to add custom logic
|
||||
* to the lifecycle of flag evaluation.
|
||||
*
|
||||
*
|
||||
* @see Hook
|
||||
*/
|
||||
public interface IntegerHook extends Hook<Integer> {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import dev.openfeature.sdk.internal.ExcludeFromGeneratedCoverageReport;
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import dev.openfeature.sdk.internal.ExcludeFromGeneratedCoverageReport;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import lombok.experimental.Delegate;
|
||||
|
|
@ -96,7 +96,6 @@ public class MutableContext implements EvaluationContext {
|
|||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve targetingKey from the context.
|
||||
*/
|
||||
|
|
@ -122,8 +121,7 @@ public class MutableContext implements EvaluationContext {
|
|||
}
|
||||
|
||||
Map<String, Value> attributes = this.asMap();
|
||||
EvaluationContext.mergeMaps(
|
||||
MutableStructure::new, attributes, overridingContext.asUnmodifiableMap());
|
||||
EvaluationContext.mergeMaps(MutableStructure::new, attributes, overridingContext.asUnmodifiableMap());
|
||||
return new MutableContext(attributes);
|
||||
}
|
||||
|
||||
|
|
@ -134,7 +132,8 @@ public class MutableContext implements EvaluationContext {
|
|||
private static class DelegateExclusions {
|
||||
|
||||
@ExcludeFromGeneratedCoverageReport
|
||||
public <T extends Structure> Map<String, Value> merge(Function<Map<String, Value>, Structure> newStructure,
|
||||
public <T extends Structure> Map<String, Value> merge(
|
||||
Function<Map<String, Value>, Structure> newStructure,
|
||||
Map<String, Value> base,
|
||||
Map<String, Value> overriding) {
|
||||
|
||||
|
|
|
|||
|
|
@ -5,14 +5,13 @@ import java.util.HashMap;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* {@link MutableStructure} represents a potentially nested object type which is used to represent
|
||||
* {@link MutableStructure} represents a potentially nested object type which is used to represent
|
||||
* structured data.
|
||||
* The MutableStructure is a Structure implementation which is not threadsafe, and whose attributes can
|
||||
* The MutableStructure is a Structure implementation which is not threadsafe, and whose attributes can
|
||||
* be modified after instantiation.
|
||||
*/
|
||||
@ToString
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import dev.openfeature.sdk.internal.ExcludeFromGeneratedCoverageReport;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import lombok.experimental.Delegate;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import lombok.experimental.Delegate;
|
||||
|
||||
/**
|
||||
* MutableTrackingEventDetails represents data pertinent to a particular tracking event.
|
||||
|
|
@ -19,6 +18,7 @@ import java.util.function.Function;
|
|||
public class MutableTrackingEventDetails implements TrackingEventDetails {
|
||||
|
||||
private final Number value;
|
||||
|
||||
@Delegate(excludes = MutableTrackingEventDetails.DelegateExclusions.class)
|
||||
private final MutableStructure structure;
|
||||
|
||||
|
|
@ -81,13 +81,13 @@ public class MutableTrackingEventDetails implements TrackingEventDetails {
|
|||
return this;
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("all")
|
||||
private static class DelegateExclusions {
|
||||
@ExcludeFromGeneratedCoverageReport
|
||||
public <T extends Structure> Map<String, Value> merge(Function<Map<String, Value>, Structure> newStructure,
|
||||
Map<String, Value> base,
|
||||
Map<String, Value> overriding) {
|
||||
public <T extends Structure> Map<String, Value> merge(
|
||||
Function<Map<String, Value>, Structure> newStructure,
|
||||
Map<String, Value> base,
|
||||
Map<String, Value> overriding) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import lombok.Getter;
|
|||
*/
|
||||
public class NoOpProvider implements FeatureProvider {
|
||||
public static final String PASSED_IN_DEFAULT = "Passed in default";
|
||||
|
||||
@Getter
|
||||
private final String name = "No-op Provider";
|
||||
|
||||
|
|
@ -58,8 +59,8 @@ public class NoOpProvider implements FeatureProvider {
|
|||
}
|
||||
|
||||
@Override
|
||||
public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue,
|
||||
EvaluationContext invocationContext) {
|
||||
public ProviderEvaluation<Value> getObjectEvaluation(
|
||||
String key, Value defaultValue, EvaluationContext invocationContext) {
|
||||
return ProviderEvaluation.<Value>builder()
|
||||
.value(defaultValue)
|
||||
.variant(PASSED_IN_DEFAULT)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ public class NoOpTransactionContextPropagator implements TransactionContextPropa
|
|||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @return empty immutable context
|
||||
*/
|
||||
@Override
|
||||
|
|
@ -18,7 +19,5 @@ public class NoOpTransactionContextPropagator implements TransactionContextPropa
|
|||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void setTransactionContext(EvaluationContext evaluationContext) {
|
||||
|
||||
}
|
||||
public void setTransactionContext(EvaluationContext evaluationContext) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,13 @@ package dev.openfeature.sdk;
|
|||
import dev.openfeature.sdk.exceptions.OpenFeatureError;
|
||||
import dev.openfeature.sdk.internal.AutoCloseableLock;
|
||||
import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* A global singleton which holds base configuration for the OpenFeature
|
||||
|
|
@ -102,11 +105,7 @@ public class OpenFeatureAPI implements EventBus<OpenFeatureAPI> {
|
|||
* @return a new client instance
|
||||
*/
|
||||
public Client getClient(String domain, String version) {
|
||||
return new OpenFeatureClient(
|
||||
this,
|
||||
domain,
|
||||
version
|
||||
);
|
||||
return new OpenFeatureClient(this, domain, version);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -196,7 +195,8 @@ public class OpenFeatureAPI implements EventBus<OpenFeatureAPI> {
|
|||
*/
|
||||
public void setProvider(String domain, FeatureProvider provider) {
|
||||
try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
|
||||
providerRepository.setProvider(domain,
|
||||
providerRepository.setProvider(
|
||||
domain,
|
||||
provider,
|
||||
this::attachEventProvider,
|
||||
this::emitReady,
|
||||
|
|
@ -229,7 +229,8 @@ public class OpenFeatureAPI implements EventBus<OpenFeatureAPI> {
|
|||
*/
|
||||
public void setProviderAndWait(String domain, FeatureProvider provider) throws OpenFeatureError {
|
||||
try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
|
||||
providerRepository.setProvider(domain,
|
||||
providerRepository.setProvider(
|
||||
domain,
|
||||
provider,
|
||||
this::attachEventProvider,
|
||||
this::emitReady,
|
||||
|
|
@ -248,7 +249,10 @@ public class OpenFeatureAPI implements EventBus<OpenFeatureAPI> {
|
|||
}
|
||||
|
||||
private void emitReady(FeatureProvider provider) {
|
||||
runHandlersForProvider(provider, ProviderEvent.PROVIDER_READY, ProviderEventDetails.builder().build());
|
||||
runHandlersForProvider(
|
||||
provider,
|
||||
ProviderEvent.PROVIDER_READY,
|
||||
ProviderEventDetails.builder().build());
|
||||
}
|
||||
|
||||
private void detachEventProvider(FeatureProvider provider) {
|
||||
|
|
@ -258,7 +262,9 @@ public class OpenFeatureAPI implements EventBus<OpenFeatureAPI> {
|
|||
}
|
||||
|
||||
private void emitError(FeatureProvider provider, OpenFeatureError exception) {
|
||||
runHandlersForProvider(provider, ProviderEvent.PROVIDER_ERROR,
|
||||
runHandlersForProvider(
|
||||
provider,
|
||||
ProviderEvent.PROVIDER_ERROR,
|
||||
ProviderEventDetails.builder().message(exception.getMessage()).build());
|
||||
}
|
||||
|
||||
|
|
@ -394,8 +400,10 @@ public class OpenFeatureAPI implements EventBus<OpenFeatureAPI> {
|
|||
try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
|
||||
// if the provider is in the state associated with event, run immediately
|
||||
if (Optional.ofNullable(this.providerRepository.getProviderState(domain))
|
||||
.orElse(ProviderState.READY).matchesEvent(event)) {
|
||||
eventSupport.runHandler(handler, EventDetails.builder().domain(domain).build());
|
||||
.orElse(ProviderState.READY)
|
||||
.matchesEvent(event)) {
|
||||
eventSupport.runHandler(
|
||||
handler, EventDetails.builder().domain(domain).build());
|
||||
}
|
||||
eventSupport.addClientHandler(domain, event, handler);
|
||||
}
|
||||
|
|
@ -415,8 +423,7 @@ public class OpenFeatureAPI implements EventBus<OpenFeatureAPI> {
|
|||
private void runHandlersForProvider(FeatureProvider provider, ProviderEvent event, ProviderEventDetails details) {
|
||||
try (AutoCloseableLock __ = lock.readLockAutoCloseable()) {
|
||||
|
||||
List<String> domainsForProvider = providerRepository
|
||||
.getDomainsForProvider(provider);
|
||||
List<String> domainsForProvider = providerRepository.getDomainsForProvider(provider);
|
||||
|
||||
final String providerName = Optional.ofNullable(provider.getMetadata())
|
||||
.map(metadata -> metadata.getName())
|
||||
|
|
@ -427,8 +434,8 @@ public class OpenFeatureAPI implements EventBus<OpenFeatureAPI> {
|
|||
|
||||
// run the handlers associated with domains for this provider
|
||||
domainsForProvider.forEach(domain -> {
|
||||
eventSupport.runClientHandlers(domain, event,
|
||||
EventDetails.fromProviderEventDetails(details, providerName, domain));
|
||||
eventSupport.runClientHandlers(
|
||||
domain, event, EventDetails.fromProviderEventDetails(details, providerName, domain));
|
||||
});
|
||||
|
||||
if (providerRepository.isDefaultProvider(provider)) {
|
||||
|
|
@ -437,8 +444,8 @@ public class OpenFeatureAPI implements EventBus<OpenFeatureAPI> {
|
|||
Set<String> boundDomains = providerRepository.getAllBoundDomains();
|
||||
allDomainNames.removeAll(boundDomains);
|
||||
allDomainNames.forEach(domain -> {
|
||||
eventSupport.runClientHandlers(domain, event,
|
||||
EventDetails.fromProviderEventDetails(details, providerName, domain));
|
||||
eventSupport.runClientHandlers(
|
||||
domain, event, EventDetails.fromProviderEventDetails(details, providerName, domain));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,6 @@ import dev.openfeature.sdk.exceptions.ProviderNotReadyError;
|
|||
import dev.openfeature.sdk.internal.AutoCloseableLock;
|
||||
import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock;
|
||||
import dev.openfeature.sdk.internal.ObjectUtils;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
|
@ -19,6 +16,8 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* OpenFeature Client implementation.
|
||||
|
|
@ -29,16 +28,24 @@ import java.util.function.Consumer;
|
|||
* @deprecated // TODO: eventually we will make this non-public. See issue #872
|
||||
*/
|
||||
@Slf4j
|
||||
@SuppressWarnings({"PMD.DataflowAnomalyAnalysis", "PMD.BeanMembersShouldSerialize", "PMD.UnusedLocalVariable",
|
||||
"unchecked", "rawtypes"})
|
||||
@SuppressWarnings({
|
||||
"PMD.DataflowAnomalyAnalysis",
|
||||
"PMD.BeanMembersShouldSerialize",
|
||||
"PMD.UnusedLocalVariable",
|
||||
"unchecked",
|
||||
"rawtypes"
|
||||
})
|
||||
@Deprecated() // TODO: eventually we will make this non-public. See issue #872
|
||||
public class OpenFeatureClient implements Client {
|
||||
|
||||
private final OpenFeatureAPI openfeatureApi;
|
||||
|
||||
@Getter
|
||||
private final String domain;
|
||||
|
||||
@Getter
|
||||
private final String version;
|
||||
|
||||
private final List<Hook> clientHooks;
|
||||
private final HookSupport hookSupport;
|
||||
AutoCloseableReentrantReadWriteLock hooksLock = new AutoCloseableReentrantReadWriteLock();
|
||||
|
|
@ -53,14 +60,11 @@ public class OpenFeatureClient implements Client {
|
|||
* providers (used by observability tools).
|
||||
* @param version Version of the client (used by observability tools).
|
||||
* @deprecated Do not use this constructor. It's for internal use only.
|
||||
* Clients created using it will not run event handlers.
|
||||
* Use the OpenFeatureAPI's getClient factory method instead.
|
||||
* Clients created using it will not run event handlers.
|
||||
* Use the OpenFeatureAPI's getClient factory method instead.
|
||||
*/
|
||||
@Deprecated() // TODO: eventually we will make this non-public. See issue #872
|
||||
public OpenFeatureClient(
|
||||
OpenFeatureAPI openFeatureAPI,
|
||||
String domain,
|
||||
String version) {
|
||||
public OpenFeatureClient(OpenFeatureAPI openFeatureAPI, String domain, String version) {
|
||||
this.openfeatureApi = openFeatureAPI;
|
||||
this.domain = domain;
|
||||
this.version = version;
|
||||
|
|
@ -85,7 +89,6 @@ public class OpenFeatureClient implements Client {
|
|||
invokeTrack(trackingEventName, null, null);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
|
|
@ -117,7 +120,6 @@ public class OpenFeatureClient implements Client {
|
|||
invokeTrack(trackingEventName, mergeEvaluationContext(context), details);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
|
|
@ -160,10 +162,10 @@ public class OpenFeatureClient implements Client {
|
|||
}
|
||||
}
|
||||
|
||||
private <T> FlagEvaluationDetails<T> evaluateFlag(FlagValueType type, String key, T defaultValue,
|
||||
EvaluationContext ctx, FlagEvaluationOptions options) {
|
||||
FlagEvaluationOptions flagOptions = ObjectUtils.defaultIfNull(options,
|
||||
() -> FlagEvaluationOptions.builder().build());
|
||||
private <T> FlagEvaluationDetails<T> evaluateFlag(
|
||||
FlagValueType type, String key, T defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
|
||||
FlagEvaluationOptions flagOptions = ObjectUtils.defaultIfNull(
|
||||
options, () -> FlagEvaluationOptions.builder().build());
|
||||
Map<String, Object> hints = Collections.unmodifiableMap(flagOptions.getHookHints());
|
||||
|
||||
FlagEvaluationDetails<T> details = null;
|
||||
|
|
@ -183,23 +185,31 @@ public class OpenFeatureClient implements Client {
|
|||
throw new FatalError("provider is in an irrecoverable error state");
|
||||
}
|
||||
|
||||
mergedHooks = ObjectUtils.merge(provider.getProviderHooks(), flagOptions.getHooks(), clientHooks,
|
||||
openfeatureApi.getHooks());
|
||||
mergedHooks = ObjectUtils.merge(
|
||||
provider.getProviderHooks(), flagOptions.getHooks(), clientHooks, openfeatureApi.getHooks());
|
||||
|
||||
EvaluationContext mergedCtx = hookSupport.beforeHooks(type, HookContext.from(key, type, this.getMetadata(),
|
||||
provider.getMetadata(), mergeEvaluationContext(ctx), defaultValue), mergedHooks, hints);
|
||||
EvaluationContext mergedCtx = hookSupport.beforeHooks(
|
||||
type,
|
||||
HookContext.from(
|
||||
key,
|
||||
type,
|
||||
this.getMetadata(),
|
||||
provider.getMetadata(),
|
||||
mergeEvaluationContext(ctx),
|
||||
defaultValue),
|
||||
mergedHooks,
|
||||
hints);
|
||||
|
||||
afterHookContext = HookContext.from(key, type, this.getMetadata(),
|
||||
provider.getMetadata(), mergedCtx, defaultValue);
|
||||
afterHookContext =
|
||||
HookContext.from(key, type, this.getMetadata(), provider.getMetadata(), mergedCtx, defaultValue);
|
||||
|
||||
ProviderEvaluation<T> providerEval = (ProviderEvaluation<T>) createProviderEvaluation(type, key,
|
||||
defaultValue, provider, mergedCtx);
|
||||
ProviderEvaluation<T> providerEval =
|
||||
(ProviderEvaluation<T>) createProviderEvaluation(type, key, defaultValue, provider, mergedCtx);
|
||||
|
||||
details = FlagEvaluationDetails.from(providerEval, key);
|
||||
if (details.getErrorCode() != null) {
|
||||
OpenFeatureError error = ExceptionUtils.instantiateErrorByErrorCode(
|
||||
details.getErrorCode(),
|
||||
details.getErrorMessage());
|
||||
OpenFeatureError error =
|
||||
ExceptionUtils.instantiateErrorByErrorCode(details.getErrorCode(), details.getErrorMessage());
|
||||
enrichDetailsWithErrorDefaults(defaultValue, details);
|
||||
hookSupport.errorHooks(type, afterHookContext, error, mergedHooks, hints);
|
||||
} else {
|
||||
|
|
@ -237,7 +247,8 @@ public class OpenFeatureClient implements Client {
|
|||
}
|
||||
|
||||
private void invokeTrack(String trackingEventName, EvaluationContext context, TrackingEventDetails details) {
|
||||
openfeatureApi.getFeatureProviderStateManager(domain)
|
||||
openfeatureApi
|
||||
.getFeatureProviderStateManager(domain)
|
||||
.getProvider()
|
||||
.track(trackingEventName, mergeEvaluationContext(context), details);
|
||||
}
|
||||
|
|
@ -262,8 +273,7 @@ public class OpenFeatureClient implements Client {
|
|||
Map merged = new HashMap<>();
|
||||
for (EvaluationContext evaluationContext : contexts) {
|
||||
if (evaluationContext != null && !evaluationContext.isEmpty()) {
|
||||
EvaluationContext.mergeMaps(ImmutableStructure::new, merged,
|
||||
evaluationContext.asUnmodifiableMap());
|
||||
EvaluationContext.mergeMaps(ImmutableStructure::new, merged, evaluationContext.asUnmodifiableMap());
|
||||
}
|
||||
}
|
||||
return new ImmutableContext(merged);
|
||||
|
|
@ -302,8 +312,8 @@ public class OpenFeatureClient implements Client {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Boolean getBooleanValue(String key, Boolean defaultValue, EvaluationContext ctx,
|
||||
FlagEvaluationOptions options) {
|
||||
public Boolean getBooleanValue(
|
||||
String key, Boolean defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
|
||||
return getBooleanDetails(key, defaultValue, ctx, options).getValue();
|
||||
}
|
||||
|
||||
|
|
@ -314,12 +324,13 @@ public class OpenFeatureClient implements Client {
|
|||
|
||||
@Override
|
||||
public FlagEvaluationDetails<Boolean> getBooleanDetails(String key, Boolean defaultValue, EvaluationContext ctx) {
|
||||
return getBooleanDetails(key, defaultValue, ctx, FlagEvaluationOptions.builder().build());
|
||||
return getBooleanDetails(
|
||||
key, defaultValue, ctx, FlagEvaluationOptions.builder().build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlagEvaluationDetails<Boolean> getBooleanDetails(String key, Boolean defaultValue, EvaluationContext ctx,
|
||||
FlagEvaluationOptions options) {
|
||||
public FlagEvaluationDetails<Boolean> getBooleanDetails(
|
||||
String key, Boolean defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
|
||||
return this.evaluateFlag(FlagValueType.BOOLEAN, key, defaultValue, ctx, options);
|
||||
}
|
||||
|
||||
|
|
@ -334,8 +345,8 @@ public class OpenFeatureClient implements Client {
|
|||
}
|
||||
|
||||
@Override
|
||||
public String getStringValue(String key, String defaultValue, EvaluationContext ctx,
|
||||
FlagEvaluationOptions options) {
|
||||
public String getStringValue(
|
||||
String key, String defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
|
||||
return getStringDetails(key, defaultValue, ctx, options).getValue();
|
||||
}
|
||||
|
||||
|
|
@ -346,12 +357,13 @@ public class OpenFeatureClient implements Client {
|
|||
|
||||
@Override
|
||||
public FlagEvaluationDetails<String> getStringDetails(String key, String defaultValue, EvaluationContext ctx) {
|
||||
return getStringDetails(key, defaultValue, ctx, FlagEvaluationOptions.builder().build());
|
||||
return getStringDetails(
|
||||
key, defaultValue, ctx, FlagEvaluationOptions.builder().build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlagEvaluationDetails<String> getStringDetails(String key, String defaultValue, EvaluationContext ctx,
|
||||
FlagEvaluationOptions options) {
|
||||
public FlagEvaluationDetails<String> getStringDetails(
|
||||
String key, String defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
|
||||
return this.evaluateFlag(FlagValueType.STRING, key, defaultValue, ctx, options);
|
||||
}
|
||||
|
||||
|
|
@ -366,8 +378,8 @@ public class OpenFeatureClient implements Client {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Integer getIntegerValue(String key, Integer defaultValue, EvaluationContext ctx,
|
||||
FlagEvaluationOptions options) {
|
||||
public Integer getIntegerValue(
|
||||
String key, Integer defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
|
||||
return getIntegerDetails(key, defaultValue, ctx, options).getValue();
|
||||
}
|
||||
|
||||
|
|
@ -378,12 +390,13 @@ public class OpenFeatureClient implements Client {
|
|||
|
||||
@Override
|
||||
public FlagEvaluationDetails<Integer> getIntegerDetails(String key, Integer defaultValue, EvaluationContext ctx) {
|
||||
return getIntegerDetails(key, defaultValue, ctx, FlagEvaluationOptions.builder().build());
|
||||
return getIntegerDetails(
|
||||
key, defaultValue, ctx, FlagEvaluationOptions.builder().build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlagEvaluationDetails<Integer> getIntegerDetails(String key, Integer defaultValue, EvaluationContext ctx,
|
||||
FlagEvaluationOptions options) {
|
||||
public FlagEvaluationDetails<Integer> getIntegerDetails(
|
||||
String key, Integer defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
|
||||
return this.evaluateFlag(FlagValueType.INTEGER, key, defaultValue, ctx, options);
|
||||
}
|
||||
|
||||
|
|
@ -398,9 +411,10 @@ public class OpenFeatureClient implements Client {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Double getDoubleValue(String key, Double defaultValue, EvaluationContext ctx,
|
||||
FlagEvaluationOptions options) {
|
||||
return this.evaluateFlag(FlagValueType.DOUBLE, key, defaultValue, ctx, options).getValue();
|
||||
public Double getDoubleValue(
|
||||
String key, Double defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
|
||||
return this.evaluateFlag(FlagValueType.DOUBLE, key, defaultValue, ctx, options)
|
||||
.getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -414,8 +428,8 @@ public class OpenFeatureClient implements Client {
|
|||
}
|
||||
|
||||
@Override
|
||||
public FlagEvaluationDetails<Double> getDoubleDetails(String key, Double defaultValue, EvaluationContext ctx,
|
||||
FlagEvaluationOptions options) {
|
||||
public FlagEvaluationDetails<Double> getDoubleDetails(
|
||||
String key, Double defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
|
||||
return this.evaluateFlag(FlagValueType.DOUBLE, key, defaultValue, ctx, options);
|
||||
}
|
||||
|
||||
|
|
@ -430,8 +444,7 @@ public class OpenFeatureClient implements Client {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Value getObjectValue(String key, Value defaultValue, EvaluationContext ctx,
|
||||
FlagEvaluationOptions options) {
|
||||
public Value getObjectValue(String key, Value defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
|
||||
return getObjectDetails(key, defaultValue, ctx, options).getValue();
|
||||
}
|
||||
|
||||
|
|
@ -441,14 +454,14 @@ public class OpenFeatureClient implements Client {
|
|||
}
|
||||
|
||||
@Override
|
||||
public FlagEvaluationDetails<Value> getObjectDetails(String key, Value defaultValue,
|
||||
EvaluationContext ctx) {
|
||||
return getObjectDetails(key, defaultValue, ctx, FlagEvaluationOptions.builder().build());
|
||||
public FlagEvaluationDetails<Value> getObjectDetails(String key, Value defaultValue, EvaluationContext ctx) {
|
||||
return getObjectDetails(
|
||||
key, defaultValue, ctx, FlagEvaluationOptions.builder().build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlagEvaluationDetails<Value> getObjectDetails(String key, Value defaultValue, EvaluationContext ctx,
|
||||
FlagEvaluationOptions options) {
|
||||
public FlagEvaluationDetails<Value> getObjectDetails(
|
||||
String key, Value defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
|
||||
return this.evaluateFlag(FlagValueType.OBJECT, key, defaultValue, ctx, options);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ public class ProviderEvaluation<T> implements BaseEvaluation<T> {
|
|||
private String reason;
|
||||
ErrorCode errorCode;
|
||||
private String errorMessage;
|
||||
|
||||
@Builder.Default
|
||||
private ImmutableMetadata flagMetadata = ImmutableMetadata.builder().build();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,5 +4,8 @@ package dev.openfeature.sdk;
|
|||
* Provider event types.
|
||||
*/
|
||||
public enum ProviderEvent {
|
||||
PROVIDER_READY, PROVIDER_CONFIGURATION_CHANGED, PROVIDER_ERROR, PROVIDER_STALE;
|
||||
PROVIDER_READY,
|
||||
PROVIDER_CONFIGURATION_CHANGED,
|
||||
PROVIDER_ERROR,
|
||||
PROVIDER_STALE;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
/**
|
||||
* The details of a particular event.
|
||||
*/
|
||||
@Data @SuperBuilder(toBuilder = true)
|
||||
@Data
|
||||
@SuperBuilder(toBuilder = true)
|
||||
public class ProviderEventDetails {
|
||||
private List<String> flagsChanged;
|
||||
private String message;
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ package dev.openfeature.sdk;
|
|||
|
||||
import dev.openfeature.sdk.exceptions.GeneralError;
|
||||
import dev.openfeature.sdk.exceptions.OpenFeatureError;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
|
@ -16,14 +14,14 @@ import java.util.function.BiConsumer;
|
|||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
class ProviderRepository {
|
||||
|
||||
private final Map<String, FeatureProviderStateManager> stateManagers = new ConcurrentHashMap<>();
|
||||
private final AtomicReference<FeatureProviderStateManager> defaultStateManger = new AtomicReference<>(
|
||||
new FeatureProviderStateManager(new NoOpProvider())
|
||||
);
|
||||
private final AtomicReference<FeatureProviderStateManager> defaultStateManger =
|
||||
new AtomicReference<>(new FeatureProviderStateManager(new NoOpProvider()));
|
||||
private final ExecutorService taskExecutor = Executors.newCachedThreadPool(runnable -> {
|
||||
final Thread thread = new Thread(runnable);
|
||||
thread.setDaemon(true);
|
||||
|
|
@ -96,7 +94,8 @@ class ProviderRepository {
|
|||
public List<String> getDomainsForProvider(FeatureProvider provider) {
|
||||
return stateManagers.entrySet().stream()
|
||||
.filter(entry -> entry.getValue().hasSameProvider(provider))
|
||||
.map(Map.Entry::getKey).collect(Collectors.toList());
|
||||
.map(Map.Entry::getKey)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public Set<String> getAllBoundDomains() {
|
||||
|
|
@ -110,12 +109,13 @@ class ProviderRepository {
|
|||
/**
|
||||
* Set the default provider.
|
||||
*/
|
||||
public void setProvider(FeatureProvider provider,
|
||||
Consumer<FeatureProvider> afterSet,
|
||||
Consumer<FeatureProvider> afterInit,
|
||||
Consumer<FeatureProvider> afterShutdown,
|
||||
BiConsumer<FeatureProvider, OpenFeatureError> afterError,
|
||||
boolean waitForInit) {
|
||||
public void setProvider(
|
||||
FeatureProvider provider,
|
||||
Consumer<FeatureProvider> afterSet,
|
||||
Consumer<FeatureProvider> afterInit,
|
||||
Consumer<FeatureProvider> afterShutdown,
|
||||
BiConsumer<FeatureProvider, OpenFeatureError> afterError,
|
||||
boolean waitForInit) {
|
||||
if (provider == null) {
|
||||
throw new IllegalArgumentException("Provider cannot be null");
|
||||
}
|
||||
|
|
@ -130,13 +130,14 @@ class ProviderRepository {
|
|||
* @param waitForInit When true, wait for initialization to finish, then returns.
|
||||
* Otherwise, initialization happens in the background.
|
||||
*/
|
||||
public void setProvider(String domain,
|
||||
FeatureProvider provider,
|
||||
Consumer<FeatureProvider> afterSet,
|
||||
Consumer<FeatureProvider> afterInit,
|
||||
Consumer<FeatureProvider> afterShutdown,
|
||||
BiConsumer<FeatureProvider, OpenFeatureError> afterError,
|
||||
boolean waitForInit) {
|
||||
public void setProvider(
|
||||
String domain,
|
||||
FeatureProvider provider,
|
||||
Consumer<FeatureProvider> afterSet,
|
||||
Consumer<FeatureProvider> afterInit,
|
||||
Consumer<FeatureProvider> afterShutdown,
|
||||
BiConsumer<FeatureProvider, OpenFeatureError> afterError,
|
||||
boolean waitForInit) {
|
||||
if (provider == null) {
|
||||
throw new IllegalArgumentException("Provider cannot be null");
|
||||
}
|
||||
|
|
@ -146,13 +147,14 @@ class ProviderRepository {
|
|||
prepareAndInitializeProvider(domain, provider, afterSet, afterInit, afterShutdown, afterError, waitForInit);
|
||||
}
|
||||
|
||||
private void prepareAndInitializeProvider(String domain,
|
||||
FeatureProvider newProvider,
|
||||
Consumer<FeatureProvider> afterSet,
|
||||
Consumer<FeatureProvider> afterInit,
|
||||
Consumer<FeatureProvider> afterShutdown,
|
||||
BiConsumer<FeatureProvider, OpenFeatureError> afterError,
|
||||
boolean waitForInit) {
|
||||
private void prepareAndInitializeProvider(
|
||||
String domain,
|
||||
FeatureProvider newProvider,
|
||||
Consumer<FeatureProvider> afterSet,
|
||||
Consumer<FeatureProvider> afterInit,
|
||||
Consumer<FeatureProvider> afterShutdown,
|
||||
BiConsumer<FeatureProvider, OpenFeatureError> afterError,
|
||||
boolean waitForInit) {
|
||||
final FeatureProviderStateManager newStateManager;
|
||||
final FeatureProviderStateManager oldStateManager;
|
||||
|
||||
|
|
@ -195,11 +197,12 @@ class ProviderRepository {
|
|||
return null;
|
||||
}
|
||||
|
||||
private void initializeProvider(FeatureProviderStateManager newManager,
|
||||
Consumer<FeatureProvider> afterInit,
|
||||
Consumer<FeatureProvider> afterShutdown,
|
||||
BiConsumer<FeatureProvider, OpenFeatureError> afterError,
|
||||
FeatureProviderStateManager oldManager) {
|
||||
private void initializeProvider(
|
||||
FeatureProviderStateManager newManager,
|
||||
Consumer<FeatureProvider> afterInit,
|
||||
Consumer<FeatureProvider> afterShutdown,
|
||||
BiConsumer<FeatureProvider, OpenFeatureError> afterError,
|
||||
FeatureProviderStateManager oldManager) {
|
||||
try {
|
||||
if (ProviderState.NOT_READY.equals(newManager.getState())) {
|
||||
newManager.initialize(OpenFeatureAPI.getInstance().getEvaluationContext());
|
||||
|
|
@ -210,15 +213,13 @@ class ProviderRepository {
|
|||
log.error(
|
||||
"Exception when initializing feature provider {}",
|
||||
newManager.getProvider().getClass().getName(),
|
||||
e
|
||||
);
|
||||
e);
|
||||
afterError.accept(newManager.getProvider(), e);
|
||||
} catch (Exception e) {
|
||||
log.error(
|
||||
"Exception when initializing feature provider {}",
|
||||
newManager.getProvider().getClass().getName(),
|
||||
e
|
||||
);
|
||||
e);
|
||||
afterError.accept(newManager.getProvider(), new GeneralError(e));
|
||||
}
|
||||
}
|
||||
|
|
@ -238,7 +239,8 @@ class ProviderRepository {
|
|||
*/
|
||||
private boolean isStateManagerRegistered(FeatureProviderStateManager manager) {
|
||||
return manager != null
|
||||
&& (this.stateManagers.containsValue(manager) || this.defaultStateManger.get().equals(manager));
|
||||
&& (this.stateManagers.containsValue(manager)
|
||||
|| this.defaultStateManger.get().equals(manager));
|
||||
}
|
||||
|
||||
private void shutdownProvider(FeatureProviderStateManager manager) {
|
||||
|
|
@ -253,7 +255,10 @@ class ProviderRepository {
|
|||
try {
|
||||
provider.shutdown();
|
||||
} catch (Exception e) {
|
||||
log.error("Exception when shutting down feature provider {}", provider.getClass().getName(), e);
|
||||
log.error(
|
||||
"Exception when shutting down feature provider {}",
|
||||
provider.getClass().getName(),
|
||||
e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -264,8 +269,7 @@ class ProviderRepository {
|
|||
* including the default feature provider.
|
||||
*/
|
||||
public void shutdown() {
|
||||
Stream
|
||||
.concat(Stream.of(this.defaultStateManger.get()), this.stateManagers.values().stream())
|
||||
Stream.concat(Stream.of(this.defaultStateManger.get()), this.stateManagers.values().stream())
|
||||
.distinct()
|
||||
.forEach(this::shutdownProvider);
|
||||
this.stateManagers.clear();
|
||||
|
|
|
|||
|
|
@ -4,11 +4,15 @@ package dev.openfeature.sdk;
|
|||
* Indicates the state of the provider.
|
||||
*/
|
||||
public enum ProviderState {
|
||||
READY, NOT_READY, ERROR, STALE, FATAL;
|
||||
READY,
|
||||
NOT_READY,
|
||||
ERROR,
|
||||
STALE,
|
||||
FATAL;
|
||||
|
||||
/**
|
||||
* Returns true if the passed ProviderEvent maps to this ProviderState.
|
||||
*
|
||||
*
|
||||
* @param event event to compare
|
||||
* @return boolean if matches.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -4,5 +4,12 @@ package dev.openfeature.sdk;
|
|||
* Predefined resolution reasons.
|
||||
*/
|
||||
public enum Reason {
|
||||
DISABLED, SPLIT, TARGETING_MATCH, DEFAULT, UNKNOWN, CACHED, STATIC, ERROR
|
||||
DISABLED,
|
||||
SPLIT,
|
||||
TARGETING_MATCH,
|
||||
DEFAULT,
|
||||
UNKNOWN,
|
||||
CACHED,
|
||||
STATIC,
|
||||
ERROR
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package dev.openfeature.sdk;
|
|||
/**
|
||||
* An extension point which can run around flag resolution. They are intended to be used as a way to add custom logic
|
||||
* to the lifecycle of flag evaluation.
|
||||
*
|
||||
*
|
||||
* @see Hook
|
||||
*/
|
||||
public interface StringHook extends Hook<String> {
|
||||
|
|
|
|||
|
|
@ -1,23 +1,23 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import dev.openfeature.sdk.exceptions.ValueNotConvertableError;
|
||||
import static dev.openfeature.sdk.Value.objectToValue;
|
||||
|
||||
import dev.openfeature.sdk.exceptions.ValueNotConvertableError;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static dev.openfeature.sdk.Value.objectToValue;
|
||||
|
||||
/**
|
||||
* {@link Structure} represents a potentially nested object type which is used to represent
|
||||
* {@link Structure} represents a potentially nested object type which is used to represent
|
||||
* structured data.
|
||||
*/
|
||||
@SuppressWarnings("PMD.BeanMembersShouldSerialize")
|
||||
public interface Structure {
|
||||
|
||||
|
||||
/**
|
||||
* Boolean indicating if this structure is empty.
|
||||
*
|
||||
* @return boolean for emptiness
|
||||
*/
|
||||
boolean isEmpty();
|
||||
|
|
@ -51,7 +51,6 @@ public interface Structure {
|
|||
*/
|
||||
Map<String, Value> asUnmodifiableMap();
|
||||
|
||||
|
||||
/**
|
||||
* Get all values, with as a map of Object.
|
||||
*
|
||||
|
|
@ -93,20 +92,15 @@ public interface Structure {
|
|||
}
|
||||
|
||||
if (value.isList()) {
|
||||
return value.asList()
|
||||
.stream()
|
||||
.map(this::convertValue)
|
||||
.collect(Collectors.toList());
|
||||
return value.asList().stream().map(this::convertValue).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
if (value.isStructure()) {
|
||||
Structure s = value.asStructure();
|
||||
return s.asUnmodifiableMap()
|
||||
.entrySet()
|
||||
.stream()
|
||||
.collect(HashMap::new,
|
||||
(accumulated, entry) -> accumulated.put(entry.getKey(),
|
||||
convertValue(entry.getValue())),
|
||||
return s.asUnmodifiableMap().entrySet().stream()
|
||||
.collect(
|
||||
HashMap::new,
|
||||
(accumulated, entry) -> accumulated.put(entry.getKey(), convertValue(entry.getValue())),
|
||||
HashMap::putAll);
|
||||
}
|
||||
|
||||
|
|
@ -121,9 +115,9 @@ public interface Structure {
|
|||
*/
|
||||
static Structure mapToStructure(Map<String, Object> map) {
|
||||
return new MutableStructure(map.entrySet().stream()
|
||||
.collect(HashMap::new,
|
||||
(accumulated, entry) -> accumulated.put(entry.getKey(),
|
||||
objectToValue(entry.getValue())),
|
||||
.collect(
|
||||
HashMap::new,
|
||||
(accumulated, entry) -> accumulated.put(entry.getKey(), objectToValue(entry.getValue())),
|
||||
HashMap::putAll));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,5 +3,4 @@ package dev.openfeature.sdk;
|
|||
/**
|
||||
* Data pertinent to a particular tracking event.
|
||||
*/
|
||||
public interface TrackingEventDetails extends Structure {
|
||||
}
|
||||
public interface TrackingEventDetails extends Structure {}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,16 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import static dev.openfeature.sdk.Structure.mapToStructure;
|
||||
|
||||
import dev.openfeature.sdk.exceptions.TypeMismatchError;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import dev.openfeature.sdk.exceptions.TypeMismatchError;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.ToString;
|
||||
|
||||
import static dev.openfeature.sdk.Structure.mapToStructure;
|
||||
|
||||
/**
|
||||
* Values serve as a generic return type for structure data from providers.
|
||||
* Providers may deal in JSON, protobuf, XML or some other data-interchange format.
|
||||
|
|
@ -37,33 +36,34 @@ public class Value implements Cloneable {
|
|||
|
||||
/**
|
||||
* Construct a new Value with an Object.
|
||||
*
|
||||
* @param value to be wrapped.
|
||||
* @throws InstantiationException if value is not a valid type
|
||||
* (boolean, string, int, double, list, structure, instant)
|
||||
* (boolean, string, int, double, list, structure, instant)
|
||||
*/
|
||||
public Value(Object value) throws InstantiationException {
|
||||
this.innerObject = value;
|
||||
if (!this.isNull()
|
||||
&& !this.isBoolean()
|
||||
&& !this.isString()
|
||||
&& !this.isNumber()
|
||||
&& !this.isStructure()
|
||||
&& !this.isList()
|
||||
&& !this.isInstant()) {
|
||||
&& !this.isBoolean()
|
||||
&& !this.isString()
|
||||
&& !this.isNumber()
|
||||
&& !this.isStructure()
|
||||
&& !this.isList()
|
||||
&& !this.isInstant()) {
|
||||
throw new InstantiationException("Invalid value type: " + value.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
public Value(Value value) {
|
||||
this.innerObject = value.innerObject;
|
||||
this.innerObject = value.innerObject;
|
||||
}
|
||||
|
||||
public Value(Boolean value) {
|
||||
this.innerObject = value;
|
||||
this.innerObject = value;
|
||||
}
|
||||
|
||||
public Value(String value) {
|
||||
this.innerObject = value;
|
||||
this.innerObject = value;
|
||||
}
|
||||
|
||||
public Value(Integer value) {
|
||||
|
|
@ -71,69 +71,69 @@ public class Value implements Cloneable {
|
|||
}
|
||||
|
||||
public Value(Double value) {
|
||||
this.innerObject = value;
|
||||
this.innerObject = value;
|
||||
}
|
||||
|
||||
public Value(Structure value) {
|
||||
this.innerObject = value;
|
||||
this.innerObject = value;
|
||||
}
|
||||
|
||||
public Value(List<Value> value) {
|
||||
this.innerObject = value;
|
||||
this.innerObject = value;
|
||||
}
|
||||
|
||||
public Value(Instant value) {
|
||||
this.innerObject = value;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Check if this Value represents null.
|
||||
*
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public boolean isNull() {
|
||||
return this.innerObject == null;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Check if this Value represents a Boolean.
|
||||
*
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public boolean isBoolean() {
|
||||
return this.innerObject instanceof Boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Check if this Value represents a String.
|
||||
*
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public boolean isString() {
|
||||
return this.innerObject instanceof String;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Check if this Value represents a numeric value.
|
||||
*
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public boolean isNumber() {
|
||||
return this.innerObject instanceof Number;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Check if this Value represents a Structure.
|
||||
*
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public boolean isStructure() {
|
||||
return this.innerObject instanceof Structure;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
/**
|
||||
* Check if this Value represents a List of Values.
|
||||
*
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public boolean isList() {
|
||||
|
|
@ -155,87 +155,88 @@ public class Value implements Cloneable {
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Check if this Value represents an Instant.
|
||||
*
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public boolean isInstant() {
|
||||
return this.innerObject instanceof Instant;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
/**
|
||||
* Retrieve the underlying Boolean value, or null.
|
||||
*
|
||||
*
|
||||
* @return Boolean
|
||||
*/
|
||||
@edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "NP_BOOLEAN_RETURN_NULL",
|
||||
justification = "This is not a plain true/false method. It's understood it can return null.")
|
||||
@edu.umd.cs.findbugs.annotations.SuppressFBWarnings(
|
||||
value = "NP_BOOLEAN_RETURN_NULL",
|
||||
justification = "This is not a plain true/false method. It's understood it can return null.")
|
||||
public Boolean asBoolean() {
|
||||
if (this.isBoolean()) {
|
||||
return (Boolean)this.innerObject;
|
||||
return (Boolean) this.innerObject;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
/**
|
||||
* Retrieve the underlying object.
|
||||
*
|
||||
*
|
||||
* @return Object
|
||||
*/
|
||||
public Object asObject() {
|
||||
return this.innerObject;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Retrieve the underlying String value, or null.
|
||||
*
|
||||
*
|
||||
* @return String
|
||||
*/
|
||||
public String asString() {
|
||||
if (this.isString()) {
|
||||
return (String)this.innerObject;
|
||||
return (String) this.innerObject;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Retrieve the underlying numeric value as an Integer, or null.
|
||||
* If the value is not an integer, it will be rounded using Math.round().
|
||||
*
|
||||
*
|
||||
* @return Integer
|
||||
*/
|
||||
public Integer asInteger() {
|
||||
if (this.isNumber() && !this.isNull()) {
|
||||
return ((Number)this.innerObject).intValue();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the underlying numeric value as a Double, or null.
|
||||
*
|
||||
* @return Double
|
||||
*/
|
||||
public Double asDouble() {
|
||||
if (this.isNumber() && !isNull()) {
|
||||
return ((Number)this.innerObject).doubleValue();
|
||||
return ((Number) this.innerObject).intValue();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Retrieve the underlying numeric value as a Double, or null.
|
||||
*
|
||||
* @return Double
|
||||
*/
|
||||
public Double asDouble() {
|
||||
if (this.isNumber() && !isNull()) {
|
||||
return ((Number) this.innerObject).doubleValue();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the underlying Structure value, or null.
|
||||
*
|
||||
*
|
||||
* @return Structure
|
||||
*/
|
||||
public Structure asStructure() {
|
||||
if (this.isStructure()) {
|
||||
return (Structure)this.innerObject;
|
||||
return (Structure) this.innerObject;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the underlying List value, or null.
|
||||
*
|
||||
|
|
@ -249,14 +250,14 @@ public class Value implements Cloneable {
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Retrieve the underlying Instant value, or null.
|
||||
*
|
||||
*
|
||||
* @return Instant
|
||||
*/
|
||||
public Instant asInstant() {
|
||||
if (this.isInstant()) {
|
||||
return (Instant)this.innerObject;
|
||||
return (Instant) this.innerObject;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -305,9 +306,8 @@ public class Value implements Cloneable {
|
|||
} else if (object instanceof Structure) {
|
||||
return new Value((Structure) object);
|
||||
} else if (object instanceof List) {
|
||||
return new Value(((List<Object>) object).stream()
|
||||
.map(o -> objectToValue(o))
|
||||
.collect(Collectors.toList()));
|
||||
return new Value(
|
||||
((List<Object>) object).stream().map(o -> objectToValue(o)).collect(Collectors.toList()));
|
||||
} else if (object instanceof Instant) {
|
||||
return new Value((Instant) object);
|
||||
} else if (object instanceof Map) {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ public class ExceptionUtils {
|
|||
|
||||
/**
|
||||
* Creates an Error for the specific error code.
|
||||
* @param errorCode the ErrorCode to use
|
||||
*
|
||||
* @param errorCode the ErrorCode to use
|
||||
* @param errorMessage the error message to include in the returned error
|
||||
* @return the specific OpenFeatureError for the errorCode
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import lombok.experimental.StandardException;
|
|||
@StandardException
|
||||
public class FatalError extends OpenFeatureError {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Getter
|
||||
private final ErrorCode errorCode = ErrorCode.PROVIDER_FATAL;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import lombok.experimental.StandardException;
|
|||
@StandardException
|
||||
public class FlagNotFoundError extends OpenFeatureErrorWithoutStacktrace {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Getter
|
||||
private final ErrorCode errorCode = ErrorCode.FLAG_NOT_FOUND;
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import lombok.experimental.StandardException;
|
|||
@StandardException
|
||||
public class GeneralError extends OpenFeatureError {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Getter
|
||||
private final ErrorCode errorCode = ErrorCode.GENERAL;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,6 @@ import lombok.experimental.StandardException;
|
|||
public class InvalidContextError extends OpenFeatureError {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Getter private final ErrorCode errorCode = ErrorCode.INVALID_CONTEXT;
|
||||
|
||||
@Getter
|
||||
private final ErrorCode errorCode = ErrorCode.INVALID_CONTEXT;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,6 @@ import lombok.experimental.StandardException;
|
|||
public class ParseError extends OpenFeatureError {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Getter private final ErrorCode errorCode = ErrorCode.PARSE_ERROR;
|
||||
|
||||
@Getter
|
||||
private final ErrorCode errorCode = ErrorCode.PARSE_ERROR;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,5 +8,7 @@ import lombok.experimental.StandardException;
|
|||
@StandardException
|
||||
public class ProviderNotReadyError extends OpenFeatureErrorWithoutStacktrace {
|
||||
private static final long serialVersionUID = 1L;
|
||||
@Getter private final ErrorCode errorCode = ErrorCode.PROVIDER_NOT_READY;
|
||||
|
||||
@Getter
|
||||
private final ErrorCode errorCode = ErrorCode.PROVIDER_NOT_READY;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,6 @@ import lombok.experimental.StandardException;
|
|||
public class TargetingKeyMissingError extends OpenFeatureError {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Getter private final ErrorCode errorCode = ErrorCode.TARGETING_KEY_MISSING;
|
||||
|
||||
@Getter
|
||||
private final ErrorCode errorCode = ErrorCode.TARGETING_KEY_MISSING;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,6 @@ import lombok.experimental.StandardException;
|
|||
public class TypeMismatchError extends OpenFeatureError {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Getter private final ErrorCode errorCode = ErrorCode.TYPE_MISMATCH;
|
||||
|
||||
@Getter
|
||||
private final ErrorCode errorCode = ErrorCode.TYPE_MISMATCH;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import lombok.experimental.StandardException;
|
|||
@StandardException
|
||||
public class ValueNotConvertableError extends OpenFeatureError {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Getter
|
||||
private final ErrorCode errorCode = ErrorCode.GENERAL;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,9 @@ import org.slf4j.spi.LoggingEventBuilder;
|
|||
* Flag evaluation data is logged at debug and error in before/after stages and error stages, respectively.
|
||||
*/
|
||||
@Slf4j
|
||||
@edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "RV_RETURN_VALUE_IGNORED",
|
||||
justification = "we can ignore return values of chainables (builders) here")
|
||||
@edu.umd.cs.findbugs.annotations.SuppressFBWarnings(
|
||||
value = "RV_RETURN_VALUE_IGNORED",
|
||||
justification = "we can ignore return values of chainables (builders) here")
|
||||
public class LoggingHook implements Hook<Object> {
|
||||
|
||||
static final String DOMAIN_KEY = "domain";
|
||||
|
|
@ -43,6 +44,7 @@ public class LoggingHook implements Hook<Object> {
|
|||
|
||||
/**
|
||||
* Construct a new LoggingHook.
|
||||
*
|
||||
* @param includeEvaluationContext include a serialized evaluation context in the log message (defaults to false)
|
||||
*/
|
||||
public LoggingHook(boolean includeEvaluationContext) {
|
||||
|
|
@ -59,8 +61,8 @@ public class LoggingHook implements Hook<Object> {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void after(HookContext<Object> hookContext, FlagEvaluationDetails<Object> details,
|
||||
Map<String, Object> hints) {
|
||||
public void after(
|
||||
HookContext<Object> hookContext, FlagEvaluationDetails<Object> details, Map<String, Object> hints) {
|
||||
LoggingEventBuilder builder = log.atDebug()
|
||||
.addKeyValue(REASON_KEY, details.getReason())
|
||||
.addKeyValue(VARIANT_KEY, details.getVariant())
|
||||
|
|
@ -71,8 +73,7 @@ public class LoggingHook implements Hook<Object> {
|
|||
|
||||
@Override
|
||||
public void error(HookContext<Object> hookContext, Exception error, Map<String, Object> hints) {
|
||||
LoggingEventBuilder builder = log.atError()
|
||||
.addKeyValue(ERROR_MESSAGE_KEY, error.getMessage());
|
||||
LoggingEventBuilder builder = log.atError().addKeyValue(ERROR_MESSAGE_KEY, error.getMessage());
|
||||
addCommonProps(builder, hookContext);
|
||||
ErrorCode errorCode = error instanceof OpenFeatureError ? ((OpenFeatureError) error).getErrorCode() : null;
|
||||
builder.addKeyValue(ERROR_CODE_KEY, errorCode);
|
||||
|
|
@ -81,7 +82,8 @@ public class LoggingHook implements Hook<Object> {
|
|||
|
||||
private void addCommonProps(LoggingEventBuilder builder, HookContext<Object> hookContext) {
|
||||
builder.addKeyValue(DOMAIN_KEY, hookContext.getClientMetadata().getDomain())
|
||||
.addKeyValue(PROVIDER_NAME_KEY, hookContext.getProviderMetadata().getName())
|
||||
.addKeyValue(
|
||||
PROVIDER_NAME_KEY, hookContext.getProviderMetadata().getName())
|
||||
.addKeyValue(FLAG_KEY_KEY, hookContext.getFlagKey())
|
||||
.addKeyValue(DEFAULT_VALUE_KEY, hookContext.getDefaultValue());
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package dev.openfeature.sdk.internal;
|
|||
|
||||
@SuppressWarnings("checkstyle:MissingJavadocType")
|
||||
public interface AutoCloseableLock extends AutoCloseable {
|
||||
|
||||
|
||||
/**
|
||||
* Override the exception in AutoClosable.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ public class AutoCloseableReentrantReadWriteLock extends ReentrantReadWriteLock
|
|||
|
||||
/**
|
||||
* Get the single write lock as an AutoCloseableLock.
|
||||
*
|
||||
* @return unlock method ref
|
||||
*/
|
||||
public AutoCloseableLock writeLockAutoCloseable() {
|
||||
|
|
@ -19,10 +20,11 @@ public class AutoCloseableReentrantReadWriteLock extends ReentrantReadWriteLock
|
|||
|
||||
/**
|
||||
* Get the multi read lock as an AutoCloseableLock.
|
||||
*
|
||||
* @return unlock method ref
|
||||
*/
|
||||
public AutoCloseableLock readLockAutoCloseable() {
|
||||
this.readLock().lock();
|
||||
return this.readLock()::unlock;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
package dev.openfeature.sdk.internal;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* JaCoCo ignores coverage of methods annotated with any annotation with "generated" in the name.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
public @interface ExcludeFromGeneratedCoverageReport {
|
||||
}
|
||||
public @interface ExcludeFromGeneratedCoverageReport {}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import java.util.ArrayList;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
@SuppressWarnings("checkstyle:MissingJavadocType")
|
||||
|
|
@ -13,9 +12,10 @@ public class ObjectUtils {
|
|||
|
||||
/**
|
||||
* If the source param is null, return the default value.
|
||||
* @param source maybe null object
|
||||
*
|
||||
* @param source maybe null object
|
||||
* @param defaultValue thing to use if source is null
|
||||
* @param <T> list type
|
||||
* @param <T> list type
|
||||
* @return resulting object
|
||||
*/
|
||||
public static <T> List<T> defaultIfNull(List<T> source, Supplier<List<T>> defaultValue) {
|
||||
|
|
@ -27,10 +27,11 @@ public class ObjectUtils {
|
|||
|
||||
/**
|
||||
* If the source param is null, return the default value.
|
||||
* @param source maybe null object
|
||||
*
|
||||
* @param source maybe null object
|
||||
* @param defaultValue thing to use if source is null
|
||||
* @param <K> map key type
|
||||
* @param <V> map value type
|
||||
* @param <K> map key type
|
||||
* @param <V> map value type
|
||||
* @return resulting map
|
||||
*/
|
||||
public static <K, V> Map<K, V> defaultIfNull(Map<K, V> source, Supplier<Map<K, V>> defaultValue) {
|
||||
|
|
@ -42,9 +43,10 @@ public class ObjectUtils {
|
|||
|
||||
/**
|
||||
* If the source param is null, return the default value.
|
||||
* @param source maybe null object
|
||||
*
|
||||
* @param source maybe null object
|
||||
* @param defaultValue thing to use if source is null
|
||||
* @param <T> type
|
||||
* @param <T> type
|
||||
* @return resulting object
|
||||
*/
|
||||
public static <T> T defaultIfNull(T source, Supplier<T> defaultValue) {
|
||||
|
|
@ -56,8 +58,9 @@ public class ObjectUtils {
|
|||
|
||||
/**
|
||||
* Concatenate a bunch of lists.
|
||||
*
|
||||
* @param sources bunch of lists.
|
||||
* @param <T> list type
|
||||
* @param <T> list type
|
||||
* @return resulting object
|
||||
*/
|
||||
@SafeVarargs
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import java.util.Objects;
|
|||
|
||||
/**
|
||||
* Like {@link java.util.function.BiConsumer} but with 3 params.
|
||||
*
|
||||
*
|
||||
* @see java.util.function.BiConsumer
|
||||
*/
|
||||
@FunctionalInterface
|
||||
|
|
@ -35,4 +35,4 @@ public interface TriConsumer<T, U, V> {
|
|||
after.accept(t, u, v);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import dev.openfeature.sdk.EvaluationContext;
|
|||
|
||||
/**
|
||||
* Context evaluator - use for resolving flag according to evaluation context, for handling targeting.
|
||||
*
|
||||
* @param <T> expected value type
|
||||
*/
|
||||
public interface ContextEvaluator<T> {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
package dev.openfeature.sdk.providers.memory;
|
||||
|
||||
import java.util.Map;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.Singular;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Flag representation for the in-memory provider.
|
||||
*/
|
||||
|
|
@ -16,6 +15,7 @@ import java.util.Map;
|
|||
public class Flag<T> {
|
||||
@Singular
|
||||
private Map<String, Object> variants;
|
||||
|
||||
private String defaultVariant;
|
||||
private ContextEvaluator<T> contextEvaluator;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,28 @@
|
|||
package dev.openfeature.sdk.providers.memory;
|
||||
|
||||
import dev.openfeature.sdk.*;
|
||||
import dev.openfeature.sdk.exceptions.*;
|
||||
import lombok.Getter;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import dev.openfeature.sdk.EvaluationContext;
|
||||
import dev.openfeature.sdk.EventProvider;
|
||||
import dev.openfeature.sdk.Metadata;
|
||||
import dev.openfeature.sdk.ProviderEvaluation;
|
||||
import dev.openfeature.sdk.ProviderEventDetails;
|
||||
import dev.openfeature.sdk.ProviderState;
|
||||
import dev.openfeature.sdk.Reason;
|
||||
import dev.openfeature.sdk.Value;
|
||||
import dev.openfeature.sdk.exceptions.FatalError;
|
||||
import dev.openfeature.sdk.exceptions.FlagNotFoundError;
|
||||
import dev.openfeature.sdk.exceptions.GeneralError;
|
||||
import dev.openfeature.sdk.exceptions.OpenFeatureError;
|
||||
import dev.openfeature.sdk.exceptions.ProviderNotReadyError;
|
||||
import dev.openfeature.sdk.exceptions.TypeMismatchError;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import lombok.Getter;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* In-memory provider.
|
||||
|
|
@ -38,6 +49,7 @@ public class InMemoryProvider extends EventProvider {
|
|||
|
||||
/**
|
||||
* Initializes the provider.
|
||||
*
|
||||
* @param evaluationContext evaluation context
|
||||
* @throws Exception on error
|
||||
*/
|
||||
|
|
@ -60,9 +72,9 @@ public class InMemoryProvider extends EventProvider {
|
|||
this.flags.putAll(newFlags);
|
||||
|
||||
ProviderEventDetails details = ProviderEventDetails.builder()
|
||||
.flagsChanged(new ArrayList<>(flagsChanged))
|
||||
.message("flags changed")
|
||||
.build();
|
||||
.flagsChanged(new ArrayList<>(flagsChanged))
|
||||
.message("flags changed")
|
||||
.build();
|
||||
emitProviderConfigurationChanged(details);
|
||||
}
|
||||
|
||||
|
|
@ -76,46 +88,45 @@ public class InMemoryProvider extends EventProvider {
|
|||
public void updateFlag(String flagKey, Flag<?> newFlag) {
|
||||
this.flags.put(flagKey, newFlag);
|
||||
ProviderEventDetails details = ProviderEventDetails.builder()
|
||||
.flagsChanged(Collections.singletonList(flagKey))
|
||||
.message("flag added/updated")
|
||||
.build();
|
||||
.flagsChanged(Collections.singletonList(flagKey))
|
||||
.message("flag added/updated")
|
||||
.build();
|
||||
emitProviderConfigurationChanged(details);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defaultValue,
|
||||
EvaluationContext evaluationContext) {
|
||||
public ProviderEvaluation<Boolean> getBooleanEvaluation(
|
||||
String key, Boolean defaultValue, EvaluationContext evaluationContext) {
|
||||
return getEvaluation(key, evaluationContext, Boolean.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProviderEvaluation<String> getStringEvaluation(String key, String defaultValue,
|
||||
EvaluationContext evaluationContext) {
|
||||
public ProviderEvaluation<String> getStringEvaluation(
|
||||
String key, String defaultValue, EvaluationContext evaluationContext) {
|
||||
return getEvaluation(key, evaluationContext, String.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defaultValue,
|
||||
EvaluationContext evaluationContext) {
|
||||
public ProviderEvaluation<Integer> getIntegerEvaluation(
|
||||
String key, Integer defaultValue, EvaluationContext evaluationContext) {
|
||||
return getEvaluation(key, evaluationContext, Integer.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultValue,
|
||||
EvaluationContext evaluationContext) {
|
||||
public ProviderEvaluation<Double> getDoubleEvaluation(
|
||||
String key, Double defaultValue, EvaluationContext evaluationContext) {
|
||||
return getEvaluation(key, evaluationContext, Double.class);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
@Override
|
||||
public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue,
|
||||
EvaluationContext evaluationContext) {
|
||||
public ProviderEvaluation<Value> getObjectEvaluation(
|
||||
String key, Value defaultValue, EvaluationContext evaluationContext) {
|
||||
return getEvaluation(key, evaluationContext, Value.class);
|
||||
}
|
||||
|
||||
private <T> ProviderEvaluation<T> getEvaluation(
|
||||
String key, EvaluationContext evaluationContext, Class<?> expectedType
|
||||
) throws OpenFeatureError {
|
||||
String key, EvaluationContext evaluationContext, Class<?> expectedType) throws OpenFeatureError {
|
||||
if (!ProviderState.READY.equals(state)) {
|
||||
if (ProviderState.NOT_READY.equals(state)) {
|
||||
throw new ProviderNotReadyError("provider not yet initialized");
|
||||
|
|
@ -138,9 +149,9 @@ public class InMemoryProvider extends EventProvider {
|
|||
value = (T) flag.getVariants().get(flag.getDefaultVariant());
|
||||
}
|
||||
return ProviderEvaluation.<T>builder()
|
||||
.value(value)
|
||||
.variant(flag.getDefaultVariant())
|
||||
.reason(Reason.STATIC.toString())
|
||||
.build();
|
||||
.value(value)
|
||||
.variant(flag.getDefaultVariant())
|
||||
.reason(Reason.STATIC.toString())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,8 @@ public class AlwaysBrokenProvider implements FeatureProvider {
|
|||
}
|
||||
|
||||
@Override
|
||||
public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue, EvaluationContext invocationContext) {
|
||||
public ProviderEvaluation<Value> getObjectEvaluation(
|
||||
String key, Value defaultValue, EvaluationContext invocationContext) {
|
||||
throw new FlagNotFoundError(TestConstants.BROKEN_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,8 @@ public class AlwaysBrokenWithDetailsProvider implements FeatureProvider {
|
|||
}
|
||||
|
||||
@Override
|
||||
public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue, EvaluationContext invocationContext) {
|
||||
public ProviderEvaluation<Value> getObjectEvaluation(
|
||||
String key, Value defaultValue, EvaluationContext invocationContext) {
|
||||
return ProviderEvaluation.<Value>builder()
|
||||
.errorMessage(TestConstants.BROKEN_MESSAGE)
|
||||
.errorCode(ErrorCode.FLAG_NOT_FOUND)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import dev.openfeature.sdk.testutils.FeatureProviderTestUtils;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class ClientProviderMappingTest {
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -1,13 +1,5 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import dev.openfeature.sdk.fixtures.HookFixtures;
|
||||
import dev.openfeature.sdk.testutils.FeatureProviderTestUtils;
|
||||
import dev.openfeature.sdk.testutils.TestEventsProvider;
|
||||
import lombok.SneakyThrows;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
|
|
@ -15,6 +7,17 @@ import static org.mockito.ArgumentMatchers.any;
|
|||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import dev.openfeature.sdk.fixtures.HookFixtures;
|
||||
import dev.openfeature.sdk.testutils.FeatureProviderTestUtils;
|
||||
import dev.openfeature.sdk.testutils.TestEventsProvider;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import lombok.SneakyThrows;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class DeveloperExperienceTest implements HookFixtures {
|
||||
transient String flagKey = "mykey";
|
||||
|
||||
|
|
@ -49,7 +52,10 @@ class DeveloperExperienceTest implements HookFixtures {
|
|||
api.setProviderAndWait(new TestEventsProvider());
|
||||
Client client = api.getClient();
|
||||
client.addHooks(clientHook);
|
||||
Boolean retval = client.getBooleanValue(flagKey, false, null,
|
||||
Boolean retval = client.getBooleanValue(
|
||||
flagKey,
|
||||
false,
|
||||
null,
|
||||
FlagEvaluationOptions.builder().hook(evalHook).build());
|
||||
verify(clientHook, times(1)).finallyAfter(any(), any());
|
||||
verify(evalHook, times(1)).finallyAfter(any(), any());
|
||||
|
|
@ -132,7 +138,10 @@ class DeveloperExperienceTest implements HookFixtures {
|
|||
assertThat(client.getProviderState()).isEqualTo(ProviderState.READY);
|
||||
}
|
||||
|
||||
@Specification(number = "5.3.5", text = "If the provider emits an event, the value of the client's provider status MUST be updated accordingly.")
|
||||
@Specification(
|
||||
number = "5.3.5",
|
||||
text =
|
||||
"If the provider emits an event, the value of the client's provider status MUST be updated accordingly.")
|
||||
@Test
|
||||
void shouldPutTheProviderInStateErrorAfterEmittingErrorEvent() {
|
||||
String domain = "domain";
|
||||
|
|
@ -145,7 +154,10 @@ class DeveloperExperienceTest implements HookFixtures {
|
|||
assertThat(client.getProviderState()).isEqualTo(ProviderState.ERROR);
|
||||
}
|
||||
|
||||
@Specification(number = "5.3.5", text = "If the provider emits an event, the value of the client's provider status MUST be updated accordingly.")
|
||||
@Specification(
|
||||
number = "5.3.5",
|
||||
text =
|
||||
"If the provider emits an event, the value of the client's provider status MUST be updated accordingly.")
|
||||
@Test
|
||||
void shouldPutTheProviderInStateStaleAfterEmittingStaleEvent() {
|
||||
String domain = "domain";
|
||||
|
|
@ -158,7 +170,10 @@ class DeveloperExperienceTest implements HookFixtures {
|
|||
assertThat(client.getProviderState()).isEqualTo(ProviderState.STALE);
|
||||
}
|
||||
|
||||
@Specification(number = "5.3.5", text = "If the provider emits an event, the value of the client's provider status MUST be updated accordingly.")
|
||||
@Specification(
|
||||
number = "5.3.5",
|
||||
text =
|
||||
"If the provider emits an event, the value of the client's provider status MUST be updated accordingly.")
|
||||
@Test
|
||||
void shouldPutTheProviderInStateReadyAfterEmittingReadyEvent() {
|
||||
String domain = "domain";
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ class DoSomethingProvider implements FeatureProvider {
|
|||
|
||||
static final String name = "Something";
|
||||
// Flag evaluation metadata
|
||||
static final ImmutableMetadata DEFAULT_METADATA = ImmutableMetadata.builder().build();
|
||||
static final ImmutableMetadata DEFAULT_METADATA =
|
||||
ImmutableMetadata.builder().build();
|
||||
private ImmutableMetadata flagMetadata;
|
||||
|
||||
public DoSomethingProvider() {
|
||||
|
|
@ -53,7 +54,8 @@ class DoSomethingProvider implements FeatureProvider {
|
|||
}
|
||||
|
||||
@Override
|
||||
public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue, EvaluationContext invocationContext) {
|
||||
public ProviderEvaluation<Value> getObjectEvaluation(
|
||||
String key, Value defaultValue, EvaluationContext invocationContext) {
|
||||
return ProviderEvaluation.<Value>builder()
|
||||
.value(null)
|
||||
.flagMetadata(flagMetadata)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static dev.openfeature.sdk.EvaluationContext.TARGETING_KEY;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
|
@ -8,23 +9,26 @@ import java.util.ArrayList;
|
|||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static dev.openfeature.sdk.EvaluationContext.TARGETING_KEY;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class EvalContextTest {
|
||||
@Specification(number="3.1.1",
|
||||
text="The `evaluation context` structure **MUST** define an optional `targeting key` field of " +
|
||||
"type string, identifying the subject of the flag evaluation.")
|
||||
@Test void requires_targeting_key() {
|
||||
@Specification(
|
||||
number = "3.1.1",
|
||||
text = "The `evaluation context` structure **MUST** define an optional `targeting key` field of "
|
||||
+ "type string, identifying the subject of the flag evaluation.")
|
||||
@Test
|
||||
void requires_targeting_key() {
|
||||
EvaluationContext ec = new ImmutableContext("targeting-key", new HashMap<>());
|
||||
assertEquals("targeting-key", ec.getTargetingKey());
|
||||
}
|
||||
|
||||
@Specification(number="3.1.2", text= "The evaluation context MUST support the inclusion of " +
|
||||
"custom fields, having keys of type `string`, and " +
|
||||
"values of type `boolean | string | number | datetime | structure`.")
|
||||
@Test void eval_context() {
|
||||
@Specification(
|
||||
number = "3.1.2",
|
||||
text = "The evaluation context MUST support the inclusion of "
|
||||
+ "custom fields, having keys of type `string`, and "
|
||||
+ "values of type `boolean | string | number | datetime | structure`.")
|
||||
@Test
|
||||
void eval_context() {
|
||||
Map<String, Value> attributes = new HashMap<>();
|
||||
Instant dt = Instant.now().truncatedTo(ChronoUnit.MILLIS);
|
||||
attributes.put("str", new Value("test"));
|
||||
|
|
@ -42,16 +46,21 @@ public class EvalContextTest {
|
|||
assertEquals(dt, ec.getValue("dt").asInstant().truncatedTo(ChronoUnit.MILLIS));
|
||||
}
|
||||
|
||||
@Specification(number="3.1.2", text="The evaluation context MUST support the inclusion of " +
|
||||
"custom fields, having keys of type `string`, and " +
|
||||
"values of type `boolean | string | number | datetime | structure`.")
|
||||
@Test void eval_context_structure_array() {
|
||||
@Specification(
|
||||
number = "3.1.2",
|
||||
text = "The evaluation context MUST support the inclusion of "
|
||||
+ "custom fields, having keys of type `string`, and "
|
||||
+ "values of type `boolean | string | number | datetime | structure`.")
|
||||
@Test
|
||||
void eval_context_structure_array() {
|
||||
Map<String, Value> attributes = new HashMap<>();
|
||||
attributes.put("obj", new Value(new MutableStructure().add("val1", 1).add("val2", "2")));
|
||||
List<Value> values = new ArrayList<Value>(){{
|
||||
add(new Value("one"));
|
||||
add(new Value("two"));
|
||||
}};
|
||||
List<Value> values = new ArrayList<Value>() {
|
||||
{
|
||||
add(new Value("one"));
|
||||
add(new Value("two"));
|
||||
}
|
||||
};
|
||||
attributes.put("arr", new Value(values));
|
||||
EvaluationContext ec = new ImmutableContext(attributes);
|
||||
|
||||
|
|
@ -64,11 +73,16 @@ public class EvalContextTest {
|
|||
assertEquals("two", arr.get(1).asString());
|
||||
}
|
||||
|
||||
@Specification(number="3.1.3", text="The evaluation context MUST support fetching the custom fields by key and also fetching all key value pairs.")
|
||||
@Test void fetch_all() {
|
||||
Map<String, Value> attributes = new HashMap<>();
|
||||
@Specification(
|
||||
number = "3.1.3",
|
||||
text =
|
||||
"The evaluation context MUST support fetching the custom fields by key and also fetching all key value pairs.")
|
||||
@Test
|
||||
void fetch_all() {
|
||||
Map<String, Value> attributes = new HashMap<>();
|
||||
Instant dt = Instant.now();
|
||||
MutableStructure mutableStructure = new MutableStructure().add("val1", 1).add("val2", "2");
|
||||
MutableStructure mutableStructure =
|
||||
new MutableStructure().add("val1", 1).add("val2", "2");
|
||||
attributes.put("str", new Value("test"));
|
||||
attributes.put("str2", new Value("test2"));
|
||||
attributes.put("bool", new Value(true));
|
||||
|
|
@ -96,8 +110,9 @@ public class EvalContextTest {
|
|||
assertEquals("2", foundObj.getValue("val2").asString());
|
||||
}
|
||||
|
||||
@Specification(number="3.1.4", text="The evaluation context fields MUST have an unique key.")
|
||||
@Test void unique_key_across_types() {
|
||||
@Specification(number = "3.1.4", text = "The evaluation context fields MUST have an unique key.")
|
||||
@Test
|
||||
void unique_key_across_types() {
|
||||
MutableContext ec = new MutableContext();
|
||||
ec.add("key", "val");
|
||||
ec.add("key", "val2");
|
||||
|
|
@ -107,8 +122,9 @@ public class EvalContextTest {
|
|||
assertEquals(3, ec.getValue("key").asInteger());
|
||||
}
|
||||
|
||||
@Test void unique_key_across_types_immutableContext() {
|
||||
HashMap<String, Value> attributes = new HashMap<>();
|
||||
@Test
|
||||
void unique_key_across_types_immutableContext() {
|
||||
HashMap<String, Value> attributes = new HashMap<>();
|
||||
attributes.put("key", new Value("val"));
|
||||
attributes.put("key", new Value("val2"));
|
||||
attributes.put("key", new Value(3));
|
||||
|
|
@ -117,23 +133,23 @@ public class EvalContextTest {
|
|||
assertEquals(3, ec.getValue("key").asInteger());
|
||||
}
|
||||
|
||||
@Test void can_chain_attribute_addition() {
|
||||
@Test
|
||||
void can_chain_attribute_addition() {
|
||||
MutableContext ec = new MutableContext();
|
||||
MutableContext out = ec.add("str", "test")
|
||||
.add("int", 4)
|
||||
.add("bool", false)
|
||||
.add("str", new MutableStructure());
|
||||
MutableContext out =
|
||||
ec.add("str", "test").add("int", 4).add("bool", false).add("str", new MutableStructure());
|
||||
assertEquals(MutableContext.class, out.getClass());
|
||||
}
|
||||
|
||||
@Test void can_add_key_with_null() {
|
||||
@Test
|
||||
void can_add_key_with_null() {
|
||||
MutableContext ec = new MutableContext()
|
||||
.add("Boolean", (Boolean)null)
|
||||
.add("String", (String)null)
|
||||
.add("Double", (Double)null)
|
||||
.add("Structure", (MutableStructure)null)
|
||||
.add("List", (List<Value>)null)
|
||||
.add("Instant", (Instant)null);
|
||||
.add("Boolean", (Boolean) null)
|
||||
.add("String", (String) null)
|
||||
.add("Double", (Double) null)
|
||||
.add("Structure", (MutableStructure) null)
|
||||
.add("List", (List<Value>) null)
|
||||
.add("Instant", (Instant) null);
|
||||
assertEquals(6, ec.asMap().size());
|
||||
assertEquals(null, ec.getValue("Boolean").asBoolean());
|
||||
assertEquals(null, ec.getValue("String").asString());
|
||||
|
|
@ -143,7 +159,8 @@ public class EvalContextTest {
|
|||
assertEquals(null, ec.getValue("Instant").asString());
|
||||
}
|
||||
|
||||
@Test void Immutable_context_merge_targeting_key() {
|
||||
@Test
|
||||
void Immutable_context_merge_targeting_key() {
|
||||
String key1 = "key1";
|
||||
EvaluationContext ctx1 = new ImmutableContext(key1, new HashMap<>());
|
||||
EvaluationContext ctx2 = new ImmutableContext(new HashMap<>());
|
||||
|
|
@ -156,19 +173,21 @@ public class EvalContextTest {
|
|||
ctxMerged = ctx1.merge(ctx2);
|
||||
assertEquals(key2, ctxMerged.getTargetingKey());
|
||||
|
||||
ctx2 = new ImmutableContext(" ",new HashMap<>());
|
||||
ctx2 = new ImmutableContext(" ", new HashMap<>());
|
||||
ctxMerged = ctx1.merge(ctx2);
|
||||
assertEquals(key1, ctxMerged.getTargetingKey());
|
||||
}
|
||||
|
||||
@Test void merge_null_returns_value() {
|
||||
@Test
|
||||
void merge_null_returns_value() {
|
||||
MutableContext ctx1 = new MutableContext("key");
|
||||
ctx1.add("mything", "value");
|
||||
EvaluationContext result = ctx1.merge(null);
|
||||
assertEquals(ctx1, result);
|
||||
}
|
||||
|
||||
@Test void merge_targeting_key() {
|
||||
@Test
|
||||
void merge_targeting_key() {
|
||||
String key1 = "key1";
|
||||
MutableContext ctx1 = new MutableContext(key1);
|
||||
MutableContext ctx2 = new MutableContext();
|
||||
|
|
@ -186,14 +205,15 @@ public class EvalContextTest {
|
|||
assertEquals(key2, ctxMerged.getTargetingKey());
|
||||
}
|
||||
|
||||
@Test void asObjectMap() {
|
||||
@Test
|
||||
void asObjectMap() {
|
||||
String key1 = "key1";
|
||||
MutableContext ctx = new MutableContext(key1);
|
||||
ctx.add("stringItem", "stringValue");
|
||||
ctx.add("boolItem", false);
|
||||
ctx.add("integerItem", 1);
|
||||
ctx.add("doubleItem", 1.2);
|
||||
ctx.add("instantItem", Instant.ofEpochSecond(1663331342));
|
||||
ctx.add("instantItem", Instant.ofEpochSecond(1663331342));
|
||||
List<Value> listItem = new ArrayList<>();
|
||||
listItem.add(new Value("item1"));
|
||||
listItem.add(new Value("item2"));
|
||||
|
|
@ -207,18 +227,17 @@ public class EvalContextTest {
|
|||
structureValue.put("structBoolItem", new Value(false));
|
||||
structureValue.put("structIntegerItem", new Value(1));
|
||||
structureValue.put("structDoubleItem", new Value(1.2));
|
||||
structureValue.put("structInstantItem", new Value(Instant.ofEpochSecond(1663331342)));
|
||||
structureValue.put("structInstantItem", new Value(Instant.ofEpochSecond(1663331342)));
|
||||
Structure structure = new MutableStructure(structureValue);
|
||||
ctx.add("structureItem", structure);
|
||||
|
||||
|
||||
Map<String, Object> want = new HashMap<>();
|
||||
want.put(TARGETING_KEY, key1);
|
||||
want.put("stringItem", "stringValue");
|
||||
want.put("boolItem", false);
|
||||
want.put("integerItem", 1);
|
||||
want.put("doubleItem", 1.2);
|
||||
want.put("instantItem", Instant.ofEpochSecond(1663331342));
|
||||
want.put("instantItem", Instant.ofEpochSecond(1663331342));
|
||||
List<String> wantListItem = new ArrayList<>();
|
||||
wantListItem.add("item1");
|
||||
wantListItem.add("item2");
|
||||
|
|
@ -232,9 +251,9 @@ public class EvalContextTest {
|
|||
wantStructureValue.put("structBoolItem", false);
|
||||
wantStructureValue.put("structIntegerItem", 1);
|
||||
wantStructureValue.put("structDoubleItem", 1.2);
|
||||
wantStructureValue.put("structInstantItem", Instant.ofEpochSecond(1663331342));
|
||||
want.put("structureItem",wantStructureValue);
|
||||
wantStructureValue.put("structInstantItem", Instant.ofEpochSecond(1663331342));
|
||||
want.put("structureItem", wantStructureValue);
|
||||
|
||||
assertEquals(want,ctx.asObjectMap());
|
||||
assertEquals(want, ctx.asObjectMap());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,15 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import dev.openfeature.sdk.exceptions.FatalError;
|
||||
import dev.openfeature.sdk.exceptions.GeneralError;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import dev.openfeature.sdk.internal.TriConsumer;
|
||||
import lombok.SneakyThrows;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class EventProviderTest {
|
||||
|
||||
private TestEventProvider eventProvider;
|
||||
|
|
@ -71,7 +66,6 @@ class EventProviderTest {
|
|||
assertThrows(IllegalStateException.class, () -> eventProvider.attach(onEmit2));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
@DisplayName("should not throw if second same onEmit attached")
|
||||
void doesNotThrowWhenOnEmitSame() {
|
||||
|
|
@ -91,32 +85,29 @@ class EventProviderTest {
|
|||
}
|
||||
|
||||
@Override
|
||||
public ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defaultValue,
|
||||
EvaluationContext ctx) {
|
||||
public ProviderEvaluation<Boolean> getBooleanEvaluation(
|
||||
String key, Boolean defaultValue, EvaluationContext ctx) {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'getBooleanEvaluation'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProviderEvaluation<String> getStringEvaluation(String key, String defaultValue,
|
||||
EvaluationContext ctx) {
|
||||
public ProviderEvaluation<String> getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'getStringEvaluation'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defaultValue,
|
||||
EvaluationContext ctx) {
|
||||
public ProviderEvaluation<Integer> getIntegerEvaluation(
|
||||
String key, Integer defaultValue, EvaluationContext ctx) {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'getIntegerEvaluation'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultValue,
|
||||
EvaluationContext ctx) {
|
||||
public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'getDoubleEvaluation'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue,
|
||||
EvaluationContext ctx) {
|
||||
public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'getObjectEvaluation'");
|
||||
}
|
||||
|
||||
|
|
@ -130,4 +121,4 @@ class EventProviderTest {
|
|||
private TriConsumer<EventProvider, ProviderEvent, ProviderEventDetails> mockOnEmit() {
|
||||
return (TriConsumer<EventProvider, ProviderEvent, ProviderEventDetails>) mock(TriConsumer.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,22 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import dev.openfeature.sdk.testutils.TestEventsProvider;
|
||||
import io.cucumber.java.AfterAll;
|
||||
import lombok.SneakyThrows;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentMatcher;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.awaitility.Awaitility.await;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import dev.openfeature.sdk.testutils.TestEventsProvider;
|
||||
import io.cucumber.java.AfterAll;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import lombok.SneakyThrows;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentMatcher;
|
||||
|
||||
class EventsTest {
|
||||
|
||||
private static final int TIMEOUT = 300;
|
||||
|
|
@ -41,8 +40,10 @@ class EventsTest {
|
|||
|
||||
@Test
|
||||
@DisplayName("should fire initial READY event when provider init succeeds")
|
||||
@Specification(number = "5.3.1", text = "If the provider's initialize function terminates normally," +
|
||||
" PROVIDER_READY handlers MUST run.")
|
||||
@Specification(
|
||||
number = "5.3.1",
|
||||
text = "If the provider's initialize function terminates normally,"
|
||||
+ " PROVIDER_READY handlers MUST run.")
|
||||
void apiInitReady() {
|
||||
final Consumer<EventDetails> handler = mockHandler();
|
||||
final String name = "apiInitReady";
|
||||
|
|
@ -50,14 +51,15 @@ class EventsTest {
|
|||
TestEventsProvider provider = new TestEventsProvider(INIT_DELAY);
|
||||
OpenFeatureAPI.getInstance().onProviderReady(handler);
|
||||
OpenFeatureAPI.getInstance().setProviderAndWait(name, provider);
|
||||
verify(handler, timeout(TIMEOUT).atLeastOnce())
|
||||
.accept(any());
|
||||
verify(handler, timeout(TIMEOUT).atLeastOnce()).accept(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fire initial ERROR event when provider init errors")
|
||||
@Specification(number = "5.3.2", text = "If the provider's initialize function terminates abnormally," +
|
||||
" PROVIDER_ERROR handlers MUST run.")
|
||||
@Specification(
|
||||
number = "5.3.2",
|
||||
text = "If the provider's initialize function terminates abnormally,"
|
||||
+ " PROVIDER_ERROR handlers MUST run.")
|
||||
void apiInitError() {
|
||||
final Consumer<EventDetails> handler = mockHandler();
|
||||
final String name = "apiInitError";
|
||||
|
|
@ -78,9 +80,10 @@ class EventsTest {
|
|||
|
||||
@Test
|
||||
@DisplayName("should propagate events")
|
||||
@Specification(number = "5.1.2", text = "When a provider signals the occurrence of a particular event, "
|
||||
+
|
||||
"the associated client and API event handlers MUST run.")
|
||||
@Specification(
|
||||
number = "5.1.2",
|
||||
text = "When a provider signals the occurrence of a particular event, "
|
||||
+ "the associated client and API event handlers MUST run.")
|
||||
void apiShouldPropagateEvents() {
|
||||
final Consumer<EventDetails> handler = mockHandler();
|
||||
final String name = "apiShouldPropagateEvents";
|
||||
|
|
@ -89,18 +92,24 @@ class EventsTest {
|
|||
OpenFeatureAPI.getInstance().setProviderAndWait(name, provider);
|
||||
OpenFeatureAPI.getInstance().onProviderConfigurationChanged(handler);
|
||||
|
||||
provider.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, EventDetails.builder().build());
|
||||
provider.mockEvent(
|
||||
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED,
|
||||
EventDetails.builder().build());
|
||||
verify(handler, timeout(TIMEOUT)).accept(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should support all event types")
|
||||
@Specification(number = "5.1.1", text = "The provider MAY define a mechanism for signaling the occurrence "
|
||||
+ "of one of a set of events, including PROVIDER_READY, PROVIDER_ERROR, "
|
||||
+ "PROVIDER_CONFIGURATION_CHANGED and PROVIDER_STALE, with a provider event details payload.")
|
||||
@Specification(number = "5.2.2", text = "The API MUST provide a function for associating handler functions"
|
||||
+
|
||||
" with a particular provider event type.")
|
||||
@Specification(
|
||||
number = "5.1.1",
|
||||
text =
|
||||
"The provider MAY define a mechanism for signaling the occurrence "
|
||||
+ "of one of a set of events, including PROVIDER_READY, PROVIDER_ERROR, "
|
||||
+ "PROVIDER_CONFIGURATION_CHANGED and PROVIDER_STALE, with a provider event details payload.")
|
||||
@Specification(
|
||||
number = "5.2.2",
|
||||
text = "The API MUST provide a function for associating handler functions"
|
||||
+ " with a particular provider event type.")
|
||||
void apiShouldSupportAllEventTypes() {
|
||||
final String name = "apiShouldSupportAllEventTypes";
|
||||
final Consumer<EventDetails> handler1 = mockHandler();
|
||||
|
|
@ -117,7 +126,8 @@ class EventsTest {
|
|||
OpenFeatureAPI.getInstance().onProviderError(handler4);
|
||||
|
||||
Arrays.asList(ProviderEvent.values()).stream().forEach(eventType -> {
|
||||
provider.mockEvent(eventType, ProviderEventDetails.builder().build());
|
||||
provider.mockEvent(
|
||||
eventType, ProviderEventDetails.builder().build());
|
||||
});
|
||||
|
||||
verify(handler1, timeout(TIMEOUT).atLeastOnce()).accept(any());
|
||||
|
|
@ -143,7 +153,10 @@ class EventsTest {
|
|||
|
||||
@Test
|
||||
@DisplayName("should propagate events for default provider and anonymous client")
|
||||
@Specification(number = "5.1.2", text = "When a provider signals the occurrence of a particular event, the associated client and API event handlers MUST run.")
|
||||
@Specification(
|
||||
number = "5.1.2",
|
||||
text =
|
||||
"When a provider signals the occurrence of a particular event, the associated client and API event handlers MUST run.")
|
||||
void shouldPropagateDefaultAndAnon() {
|
||||
final Consumer<EventDetails> handler = mockHandler();
|
||||
|
||||
|
|
@ -153,13 +166,17 @@ class EventsTest {
|
|||
Client client = OpenFeatureAPI.getInstance().getClient();
|
||||
client.onProviderStale(handler);
|
||||
|
||||
provider.mockEvent(ProviderEvent.PROVIDER_STALE, EventDetails.builder().build());
|
||||
provider.mockEvent(
|
||||
ProviderEvent.PROVIDER_STALE, EventDetails.builder().build());
|
||||
verify(handler, timeout(TIMEOUT)).accept(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should propagate events for default provider and named client")
|
||||
@Specification(number = "5.1.2", text = "When a provider signals the occurrence of a particular event, the associated client and API event handlers MUST run.")
|
||||
@Specification(
|
||||
number = "5.1.2",
|
||||
text =
|
||||
"When a provider signals the occurrence of a particular event, the associated client and API event handlers MUST run.")
|
||||
void shouldPropagateDefaultAndNamed() {
|
||||
final Consumer<EventDetails> handler = mockHandler();
|
||||
final String name = "shouldPropagateDefaultAndNamed";
|
||||
|
|
@ -170,7 +187,8 @@ class EventsTest {
|
|||
Client client = OpenFeatureAPI.getInstance().getClient(name);
|
||||
client.onProviderStale(handler);
|
||||
|
||||
provider.mockEvent(ProviderEvent.PROVIDER_STALE, EventDetails.builder().build());
|
||||
provider.mockEvent(
|
||||
ProviderEvent.PROVIDER_STALE, EventDetails.builder().build());
|
||||
verify(handler, timeout(TIMEOUT)).accept(any());
|
||||
}
|
||||
}
|
||||
|
|
@ -186,7 +204,10 @@ class EventsTest {
|
|||
class Initialization {
|
||||
@Test
|
||||
@DisplayName("should fire initial READY event when provider init succeeds after client retrieved")
|
||||
@Specification(number = "5.3.1", text = "If the provider's initialize function terminates normally, PROVIDER_READY handlers MUST run.")
|
||||
@Specification(
|
||||
number = "5.3.1",
|
||||
text =
|
||||
"If the provider's initialize function terminates normally, PROVIDER_READY handlers MUST run.")
|
||||
void initReadyProviderBefore() {
|
||||
final Consumer<EventDetails> handler = mockHandler();
|
||||
final String name = "initReadyProviderBefore";
|
||||
|
|
@ -202,7 +223,10 @@ class EventsTest {
|
|||
|
||||
@Test
|
||||
@DisplayName("should fire initial READY event when provider init succeeds before client retrieved")
|
||||
@Specification(number = "5.3.1", text = "If the provider's initialize function terminates normally, PROVIDER_READY handlers MUST run.")
|
||||
@Specification(
|
||||
number = "5.3.1",
|
||||
text =
|
||||
"If the provider's initialize function terminates normally, PROVIDER_READY handlers MUST run.")
|
||||
void initReadyProviderAfter() {
|
||||
final Consumer<EventDetails> handler = mockHandler();
|
||||
final String name = "initReadyProviderAfter";
|
||||
|
|
@ -218,7 +242,10 @@ class EventsTest {
|
|||
|
||||
@Test
|
||||
@DisplayName("should fire initial ERROR event when provider init errors after client retrieved")
|
||||
@Specification(number = "5.3.2", text = "If the provider's initialize function terminates abnormally, PROVIDER_ERROR handlers MUST run.")
|
||||
@Specification(
|
||||
number = "5.3.2",
|
||||
text =
|
||||
"If the provider's initialize function terminates abnormally, PROVIDER_ERROR handlers MUST run.")
|
||||
void initErrorProviderAfter() {
|
||||
final Consumer<EventDetails> handler = mockHandler();
|
||||
final String name = "initErrorProviderAfter";
|
||||
|
|
@ -230,14 +257,16 @@ class EventsTest {
|
|||
// set provider after getting a client
|
||||
OpenFeatureAPI.getInstance().setProvider(name, provider);
|
||||
verify(handler, timeout(TIMEOUT)).accept(argThat(details -> {
|
||||
return name.equals(details.getDomain())
|
||||
&& errMessage.equals(details.getMessage());
|
||||
return name.equals(details.getDomain()) && errMessage.equals(details.getMessage());
|
||||
}));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fire initial ERROR event when provider init errors before client retrieved")
|
||||
@Specification(number = "5.3.2", text = "If the provider's initialize function terminates abnormally, PROVIDER_ERROR handlers MUST run.")
|
||||
@Specification(
|
||||
number = "5.3.2",
|
||||
text =
|
||||
"If the provider's initialize function terminates abnormally, PROVIDER_ERROR handlers MUST run.")
|
||||
void initErrorProviderBefore() {
|
||||
final Consumer<EventDetails> handler = mockHandler();
|
||||
final String name = "initErrorProviderBefore";
|
||||
|
|
@ -249,8 +278,7 @@ class EventsTest {
|
|||
Client client = OpenFeatureAPI.getInstance().getClient(name);
|
||||
client.onProviderError(handler);
|
||||
verify(handler, timeout(TIMEOUT)).accept(argThat(details -> {
|
||||
return name.equals(details.getDomain())
|
||||
&& errMessage.equals(details.getMessage());
|
||||
return name.equals(details.getDomain()) && errMessage.equals(details.getMessage());
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
@ -261,7 +289,10 @@ class EventsTest {
|
|||
|
||||
@Test
|
||||
@DisplayName("should propagate events when provider set before client retrieved")
|
||||
@Specification(number = "5.1.2", text = "When a provider signals the occurrence of a particular event, the associated client and API event handlers MUST run.")
|
||||
@Specification(
|
||||
number = "5.1.2",
|
||||
text =
|
||||
"When a provider signals the occurrence of a particular event, the associated client and API event handlers MUST run.")
|
||||
void shouldPropagateBefore() {
|
||||
final Consumer<EventDetails> handler = mockHandler();
|
||||
final String name = "shouldPropagateBefore";
|
||||
|
|
@ -272,13 +303,19 @@ class EventsTest {
|
|||
Client client = OpenFeatureAPI.getInstance().getClient(name);
|
||||
client.onProviderConfigurationChanged(handler);
|
||||
|
||||
provider.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, EventDetails.builder().build());
|
||||
verify(handler, timeout(TIMEOUT)).accept(argThat(details -> details.getDomain().equals(name)));
|
||||
provider.mockEvent(
|
||||
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED,
|
||||
EventDetails.builder().build());
|
||||
verify(handler, timeout(TIMEOUT))
|
||||
.accept(argThat(details -> details.getDomain().equals(name)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should propagate events when provider set after client retrieved")
|
||||
@Specification(number = "5.1.2", text = "When a provider signals the occurrence of a particular event, the associated client and API event handlers MUST run.")
|
||||
@Specification(
|
||||
number = "5.1.2",
|
||||
text =
|
||||
"When a provider signals the occurrence of a particular event, the associated client and API event handlers MUST run.")
|
||||
void shouldPropagateAfter() {
|
||||
|
||||
final Consumer<EventDetails> handler = mockHandler();
|
||||
|
|
@ -290,18 +327,25 @@ class EventsTest {
|
|||
// set provider after getting a client
|
||||
OpenFeatureAPI.getInstance().setProviderAndWait(name, provider);
|
||||
|
||||
provider.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, EventDetails.builder().build());
|
||||
verify(handler, timeout(TIMEOUT)).accept(argThat(details -> details.getDomain().equals(name)));
|
||||
provider.mockEvent(
|
||||
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED,
|
||||
EventDetails.builder().build());
|
||||
verify(handler, timeout(TIMEOUT))
|
||||
.accept(argThat(details -> details.getDomain().equals(name)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should support all event types")
|
||||
@Specification(number = "5.1.1", text = "The provider MAY define a mechanism for signaling the occurrence "
|
||||
+ "of one of a set of events, including PROVIDER_READY, PROVIDER_ERROR, "
|
||||
+ "PROVIDER_CONFIGURATION_CHANGED and PROVIDER_STALE, with a provider event details payload.")
|
||||
@Specification(number = "5.2.1", text = "The client MUST provide a function for associating handler functions"
|
||||
+
|
||||
" with a particular provider event type.")
|
||||
@Specification(
|
||||
number = "5.1.1",
|
||||
text =
|
||||
"The provider MAY define a mechanism for signaling the occurrence "
|
||||
+ "of one of a set of events, including PROVIDER_READY, PROVIDER_ERROR, "
|
||||
+ "PROVIDER_CONFIGURATION_CHANGED and PROVIDER_STALE, with a provider event details payload.")
|
||||
@Specification(
|
||||
number = "5.2.1",
|
||||
text = "The client MUST provide a function for associating handler functions"
|
||||
+ " with a particular provider event type.")
|
||||
void shouldSupportAllEventTypes() {
|
||||
final String name = "shouldSupportAllEventTypes";
|
||||
final Consumer<EventDetails> handler1 = mockHandler();
|
||||
|
|
@ -321,8 +365,8 @@ class EventsTest {
|
|||
Arrays.asList(ProviderEvent.values()).stream().forEach(eventType -> {
|
||||
provider.mockEvent(eventType, ProviderEventDetails.builder().build());
|
||||
});
|
||||
ArgumentMatcher<EventDetails> nameMatches = (EventDetails details) -> details.getDomain()
|
||||
.equals(name);
|
||||
ArgumentMatcher<EventDetails> nameMatches =
|
||||
(EventDetails details) -> details.getDomain().equals(name);
|
||||
verify(handler1, timeout(TIMEOUT).atLeastOnce()).accept(argThat(nameMatches));
|
||||
verify(handler2, timeout(TIMEOUT).atLeastOnce()).accept(argThat(nameMatches));
|
||||
verify(handler3, timeout(TIMEOUT).atLeastOnce()).accept(argThat(nameMatches));
|
||||
|
|
@ -353,7 +397,9 @@ class EventsTest {
|
|||
await().until(() -> provider1.isShutDown());
|
||||
|
||||
// fire old event
|
||||
provider1.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, EventDetails.builder().build());
|
||||
provider1.mockEvent(
|
||||
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED,
|
||||
EventDetails.builder().build());
|
||||
|
||||
// a bit of waiting here, but we want to make sure these are indeed never
|
||||
// called.
|
||||
|
|
@ -363,8 +409,10 @@ class EventsTest {
|
|||
|
||||
@Test
|
||||
@DisplayName("other client handlers should not run")
|
||||
@Specification(number = "5.1.3", text = "When a provider signals the occurrence of a particular event, " +
|
||||
"event handlers on clients which are not associated with that provider MUST NOT run.")
|
||||
@Specification(
|
||||
number = "5.1.3",
|
||||
text = "When a provider signals the occurrence of a particular event, "
|
||||
+ "event handlers on clients which are not associated with that provider MUST NOT run.")
|
||||
void otherClientHandlersShouldNotRun() {
|
||||
final String name1 = "otherClientHandlersShouldNotRun1";
|
||||
final String name2 = "otherClientHandlersShouldNotRun2";
|
||||
|
|
@ -382,7 +430,9 @@ class EventsTest {
|
|||
client1.onProviderConfigurationChanged(handlerToRun);
|
||||
client2.onProviderConfigurationChanged(handlerNotToRun);
|
||||
|
||||
provider1.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, ProviderEventDetails.builder().build());
|
||||
provider1.mockEvent(
|
||||
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED,
|
||||
ProviderEventDetails.builder().build());
|
||||
|
||||
verify(handlerToRun, timeout(TIMEOUT)).accept(any());
|
||||
verify(handlerNotToRun, never()).accept(any());
|
||||
|
|
@ -390,8 +440,10 @@ class EventsTest {
|
|||
|
||||
@Test
|
||||
@DisplayName("bound named client handlers should not run with default")
|
||||
@Specification(number = "5.1.3", text = "When a provider signals the occurrence of a particular event, " +
|
||||
"event handlers on clients which are not associated with that provider MUST NOT run.")
|
||||
@Specification(
|
||||
number = "5.1.3",
|
||||
text = "When a provider signals the occurrence of a particular event, "
|
||||
+ "event handlers on clients which are not associated with that provider MUST NOT run.")
|
||||
void boundShouldNotRunWithDefault() {
|
||||
final String name = "boundShouldNotRunWithDefault";
|
||||
final Consumer<EventDetails> handlerNotToRun = mockHandler();
|
||||
|
|
@ -408,7 +460,9 @@ class EventsTest {
|
|||
await().until(() -> namedProvider.getState().equals(ProviderState.READY));
|
||||
|
||||
// fire event on default provider
|
||||
defaultProvider.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, ProviderEventDetails.builder().build());
|
||||
defaultProvider.mockEvent(
|
||||
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED,
|
||||
ProviderEventDetails.builder().build());
|
||||
|
||||
verify(handlerNotToRun, after(TIMEOUT).never()).accept(any());
|
||||
OpenFeatureAPI.getInstance().setProviderAndWait(new NoOpProvider());
|
||||
|
|
@ -416,8 +470,10 @@ class EventsTest {
|
|||
|
||||
@Test
|
||||
@DisplayName("unbound named client handlers should run with default")
|
||||
@Specification(number = "5.1.3", text = "When a provider signals the occurrence of a particular event, " +
|
||||
"event handlers on clients which are not associated with that provider MUST NOT run.")
|
||||
@Specification(
|
||||
number = "5.1.3",
|
||||
text = "When a provider signals the occurrence of a particular event, "
|
||||
+ "event handlers on clients which are not associated with that provider MUST NOT run.")
|
||||
void unboundShouldRunWithDefault() {
|
||||
final String name = "unboundShouldRunWithDefault";
|
||||
final Consumer<EventDetails> handlerToRun = mockHandler();
|
||||
|
|
@ -432,7 +488,9 @@ class EventsTest {
|
|||
await().until(() -> defaultProvider.getState().equals(ProviderState.READY));
|
||||
|
||||
// fire event on default provider
|
||||
defaultProvider.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, ProviderEventDetails.builder().build());
|
||||
defaultProvider.mockEvent(
|
||||
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED,
|
||||
ProviderEventDetails.builder().build());
|
||||
|
||||
verify(handlerToRun, timeout(TIMEOUT)).accept(any());
|
||||
OpenFeatureAPI.getInstance().setProviderAndWait(new NoOpProvider());
|
||||
|
|
@ -440,7 +498,9 @@ class EventsTest {
|
|||
|
||||
@Test
|
||||
@DisplayName("subsequent handlers run if earlier throws")
|
||||
@Specification(number = "5.2.5", text = "If a handler function terminates abnormally, other handler functions MUST run.")
|
||||
@Specification(
|
||||
number = "5.2.5",
|
||||
text = "If a handler function terminates abnormally, other handler functions MUST run.")
|
||||
void handlersRunIfOneThrows() {
|
||||
final String name = "handlersRunIfOneThrows";
|
||||
final Consumer<EventDetails> errorHandler = mockHandler();
|
||||
|
|
@ -457,7 +517,9 @@ class EventsTest {
|
|||
client1.onProviderConfigurationChanged(nextHandler);
|
||||
client1.onProviderConfigurationChanged(lastHandler);
|
||||
|
||||
provider.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, ProviderEventDetails.builder().build());
|
||||
provider.mockEvent(
|
||||
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED,
|
||||
ProviderEventDetails.builder().build());
|
||||
verify(errorHandler, timeout(TIMEOUT)).accept(any());
|
||||
verify(nextHandler, timeout(TIMEOUT)).accept(any());
|
||||
verify(lastHandler, timeout(TIMEOUT)).accept(any());
|
||||
|
|
@ -466,7 +528,9 @@ class EventsTest {
|
|||
@Test
|
||||
@DisplayName("should have all properties")
|
||||
@Specification(number = "5.2.4", text = "The handler function MUST accept a event details parameter.")
|
||||
@Specification(number = "5.2.3", text = "The `event details` MUST contain the `provider name` associated with the event.")
|
||||
@Specification(
|
||||
number = "5.2.3",
|
||||
text = "The `event details` MUST contain the `provider name` associated with the event.")
|
||||
void shouldHaveAllProperties() {
|
||||
final Consumer<EventDetails> handler1 = mockHandler();
|
||||
final Consumer<EventDetails> handler2 = mockHandler();
|
||||
|
|
@ -481,7 +545,8 @@ class EventsTest {
|
|||
client.onProviderConfigurationChanged(handler2);
|
||||
|
||||
List<String> flagsChanged = Arrays.asList("flag");
|
||||
ImmutableMetadata metadata = ImmutableMetadata.builder().addInteger("int", 1).build();
|
||||
ImmutableMetadata metadata =
|
||||
ImmutableMetadata.builder().addInteger("int", 1).build();
|
||||
String message = "a message";
|
||||
ProviderEventDetails details = ProviderEventDetails.builder()
|
||||
.eventMetadata(metadata)
|
||||
|
|
@ -492,25 +557,25 @@ class EventsTest {
|
|||
provider.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, details);
|
||||
|
||||
// both global and client handler should have all the fields.
|
||||
verify(handler1, timeout(TIMEOUT))
|
||||
.accept(argThat((EventDetails eventDetails) -> {
|
||||
return metadata.equals(eventDetails.getEventMetadata())
|
||||
// TODO: issue for client name in events
|
||||
&& flagsChanged.equals(eventDetails.getFlagsChanged())
|
||||
&& message.equals(eventDetails.getMessage());
|
||||
}));
|
||||
verify(handler2, timeout(TIMEOUT))
|
||||
.accept(argThat((EventDetails eventDetails) -> {
|
||||
return metadata.equals(eventDetails.getEventMetadata())
|
||||
&& flagsChanged.equals(eventDetails.getFlagsChanged())
|
||||
&& message.equals(eventDetails.getMessage())
|
||||
&& name.equals(eventDetails.getDomain());
|
||||
}));
|
||||
verify(handler1, timeout(TIMEOUT)).accept(argThat((EventDetails eventDetails) -> {
|
||||
return metadata.equals(eventDetails.getEventMetadata())
|
||||
// TODO: issue for client name in events
|
||||
&& flagsChanged.equals(eventDetails.getFlagsChanged())
|
||||
&& message.equals(eventDetails.getMessage());
|
||||
}));
|
||||
verify(handler2, timeout(TIMEOUT)).accept(argThat((EventDetails eventDetails) -> {
|
||||
return metadata.equals(eventDetails.getEventMetadata())
|
||||
&& flagsChanged.equals(eventDetails.getFlagsChanged())
|
||||
&& message.equals(eventDetails.getMessage())
|
||||
&& name.equals(eventDetails.getDomain());
|
||||
}));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("if the provider is ready handlers must run immediately")
|
||||
@Specification(number = "5.3.3", text = "Handlers attached after the provider is already in the associated state, MUST run immediately.")
|
||||
@Specification(
|
||||
number = "5.3.3",
|
||||
text = "Handlers attached after the provider is already in the associated state, MUST run immediately.")
|
||||
void matchingReadyEventsMustRunImmediately() {
|
||||
final String name = "matchingEventsMustRunImmediately";
|
||||
final Consumer<EventDetails> handler = mockHandler();
|
||||
|
|
@ -527,7 +592,9 @@ class EventsTest {
|
|||
|
||||
@Test
|
||||
@DisplayName("if the provider is ready handlers must run immediately")
|
||||
@Specification(number = "5.3.3", text = "Handlers attached after the provider is already in the associated state, MUST run immediately.")
|
||||
@Specification(
|
||||
number = "5.3.3",
|
||||
text = "Handlers attached after the provider is already in the associated state, MUST run immediately.")
|
||||
void matchingStaleEventsMustRunImmediately() {
|
||||
final String name = "matchingEventsMustRunImmediately";
|
||||
final Consumer<EventDetails> handler = mockHandler();
|
||||
|
|
@ -547,7 +614,9 @@ class EventsTest {
|
|||
|
||||
@Test
|
||||
@DisplayName("if the provider is ready handlers must run immediately")
|
||||
@Specification(number = "5.3.3", text = "Handlers attached after the provider is already in the associated state, MUST run immediately.")
|
||||
@Specification(
|
||||
number = "5.3.3",
|
||||
text = "Handlers attached after the provider is already in the associated state, MUST run immediately.")
|
||||
void matchingErrorEventsMustRunImmediately() {
|
||||
final String name = "matchingEventsMustRunImmediately";
|
||||
final Consumer<EventDetails> handler = mockHandler();
|
||||
|
|
@ -560,7 +629,6 @@ class EventsTest {
|
|||
provider.emitProviderError(ProviderEventDetails.builder().build());
|
||||
assertThat(client.getProviderState()).isEqualTo(ProviderState.ERROR);
|
||||
|
||||
|
||||
// should run even thought handler was added after error
|
||||
client.onProviderError(handler);
|
||||
verify(handler, timeout(TIMEOUT)).accept(any());
|
||||
|
|
@ -580,8 +648,11 @@ class EventsTest {
|
|||
Client client = OpenFeatureAPI.getInstance().getClient(name);
|
||||
client.onProviderConfigurationChanged(handler);
|
||||
|
||||
provider1.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, ProviderEventDetails.builder().build());
|
||||
ArgumentMatcher<EventDetails> nameMatches = (EventDetails details) -> details.getDomain().equals(name);
|
||||
provider1.mockEvent(
|
||||
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED,
|
||||
ProviderEventDetails.builder().build());
|
||||
ArgumentMatcher<EventDetails> nameMatches =
|
||||
(EventDetails details) -> details.getDomain().equals(name);
|
||||
|
||||
verify(handler, timeout(TIMEOUT).times(1)).accept(argThat(nameMatches));
|
||||
|
||||
|
|
@ -590,13 +661,17 @@ class EventsTest {
|
|||
|
||||
// verify that with the new provider under the same name, the handler is called
|
||||
// again.
|
||||
provider2.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, ProviderEventDetails.builder().build());
|
||||
provider2.mockEvent(
|
||||
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED,
|
||||
ProviderEventDetails.builder().build());
|
||||
verify(handler, timeout(TIMEOUT).times(2)).accept(argThat(nameMatches));
|
||||
}
|
||||
|
||||
@Nested
|
||||
class HandlerRemoval {
|
||||
@Specification(number = "5.2.7", text = "The API and client MUST provide a function allowing the removal of event handlers.")
|
||||
@Specification(
|
||||
number = "5.2.7",
|
||||
text = "The API and client MUST provide a function allowing the removal of event handlers.")
|
||||
@Test
|
||||
@DisplayName("should not run removed events")
|
||||
@SneakyThrows
|
||||
|
|
@ -617,18 +692,21 @@ class EventsTest {
|
|||
client.removeHandler(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, handler2);
|
||||
|
||||
// emit event
|
||||
provider.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, ProviderEventDetails.builder().build());
|
||||
|
||||
provider.mockEvent(
|
||||
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED,
|
||||
ProviderEventDetails.builder().build());
|
||||
|
||||
// both global and client handlers should not run.
|
||||
verify(handler1, after(TIMEOUT).never()).accept(any());
|
||||
verify(handler2, never()).accept(any());
|
||||
}
|
||||
}
|
||||
|
||||
@Specification(number = "5.1.4", text = "PROVIDER_ERROR events SHOULD populate the provider event details's error message field.")
|
||||
@Specification(
|
||||
number = "5.1.4",
|
||||
text = "PROVIDER_ERROR events SHOULD populate the provider event details's error message field.")
|
||||
@Test
|
||||
void thisIsAProviderRequirement() {
|
||||
}
|
||||
void thisIsAProviderRequirement() {}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Consumer<EventDetails> mockHandler() {
|
||||
|
|
|
|||
|
|
@ -1,17 +1,16 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import dev.openfeature.sdk.exceptions.FatalError;
|
||||
import dev.openfeature.sdk.exceptions.GeneralError;
|
||||
import lombok.SneakyThrows;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import dev.openfeature.sdk.exceptions.FatalError;
|
||||
import dev.openfeature.sdk.exceptions.GeneralError;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import javax.annotation.Nullable;
|
||||
import lombok.SneakyThrows;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class FeatureProviderStateManagerTest {
|
||||
|
||||
private FeatureProviderStateManager wrapper;
|
||||
|
|
@ -48,7 +47,10 @@ class FeatureProviderStateManagerTest {
|
|||
|
||||
@SneakyThrows
|
||||
@Test
|
||||
@Specification(number = "1.7.3", text = "The client's provider status accessor MUST indicate READY if the initialize function of the associated provider terminates normally.")
|
||||
@Specification(
|
||||
number = "1.7.3",
|
||||
text =
|
||||
"The client's provider status accessor MUST indicate READY if the initialize function of the associated provider terminates normally.")
|
||||
void shouldSetStateToReadyAfterInit() {
|
||||
assertThat(wrapper.getState()).isEqualTo(ProviderState.NOT_READY);
|
||||
wrapper.initialize(null);
|
||||
|
|
@ -65,7 +67,10 @@ class FeatureProviderStateManagerTest {
|
|||
assertThat(wrapper.getState()).isEqualTo(ProviderState.NOT_READY);
|
||||
}
|
||||
|
||||
@Specification(number = "1.7.4", text = "The client's provider status accessor MUST indicate ERROR if the initialize function of the associated provider terminates abnormally.")
|
||||
@Specification(
|
||||
number = "1.7.4",
|
||||
text =
|
||||
"The client's provider status accessor MUST indicate ERROR if the initialize function of the associated provider terminates abnormally.")
|
||||
@Test
|
||||
void shouldSetStateToErrorAfterErrorOnInit() {
|
||||
testDelegate.throwOnInit = new Exception();
|
||||
|
|
@ -74,7 +79,10 @@ class FeatureProviderStateManagerTest {
|
|||
assertThat(wrapper.getState()).isEqualTo(ProviderState.ERROR);
|
||||
}
|
||||
|
||||
@Specification(number = "1.7.4", text = "The client's provider status accessor MUST indicate ERROR if the initialize function of the associated provider terminates abnormally.")
|
||||
@Specification(
|
||||
number = "1.7.4",
|
||||
text =
|
||||
"The client's provider status accessor MUST indicate ERROR if the initialize function of the associated provider terminates abnormally.")
|
||||
@Test
|
||||
void shouldSetStateToErrorAfterOpenFeatureErrorOnInit() {
|
||||
testDelegate.throwOnInit = new GeneralError();
|
||||
|
|
@ -83,7 +91,10 @@ class FeatureProviderStateManagerTest {
|
|||
assertThat(wrapper.getState()).isEqualTo(ProviderState.ERROR);
|
||||
}
|
||||
|
||||
@Specification(number = "1.7.5", text = "The client's provider status accessor MUST indicate FATAL if the initialize function of the associated provider terminates abnormally and indicates error code PROVIDER_FATAL.")
|
||||
@Specification(
|
||||
number = "1.7.5",
|
||||
text =
|
||||
"The client's provider status accessor MUST indicate FATAL if the initialize function of the associated provider terminates abnormally and indicates error code PROVIDER_FATAL.")
|
||||
@Test
|
||||
void shouldSetStateToErrorAfterFatalErrorOnInit() {
|
||||
testDelegate.throwOnInit = new FatalError();
|
||||
|
|
@ -92,7 +103,10 @@ class FeatureProviderStateManagerTest {
|
|||
assertThat(wrapper.getState()).isEqualTo(ProviderState.FATAL);
|
||||
}
|
||||
|
||||
@Specification(number = "5.3.5", text = "If the provider emits an event, the value of the client's provider status MUST be updated accordingly.")
|
||||
@Specification(
|
||||
number = "5.3.5",
|
||||
text =
|
||||
"If the provider emits an event, the value of the client's provider status MUST be updated accordingly.")
|
||||
@Test
|
||||
void shouldSetTheStateToReadyWhenAReadyEventIsEmitted() {
|
||||
assertThat(wrapper.getState()).isEqualTo(ProviderState.NOT_READY);
|
||||
|
|
@ -100,7 +114,10 @@ class FeatureProviderStateManagerTest {
|
|||
assertThat(wrapper.getState()).isEqualTo(ProviderState.READY);
|
||||
}
|
||||
|
||||
@Specification(number = "5.3.5", text = "If the provider emits an event, the value of the client's provider status MUST be updated accordingly.")
|
||||
@Specification(
|
||||
number = "5.3.5",
|
||||
text =
|
||||
"If the provider emits an event, the value of the client's provider status MUST be updated accordingly.")
|
||||
@Test
|
||||
void shouldSetTheStateToStaleWhenAStaleEventIsEmitted() {
|
||||
assertThat(wrapper.getState()).isEqualTo(ProviderState.NOT_READY);
|
||||
|
|
@ -108,25 +125,31 @@ class FeatureProviderStateManagerTest {
|
|||
assertThat(wrapper.getState()).isEqualTo(ProviderState.STALE);
|
||||
}
|
||||
|
||||
@Specification(number = "5.3.5", text = "If the provider emits an event, the value of the client's provider status MUST be updated accordingly.")
|
||||
@Specification(
|
||||
number = "5.3.5",
|
||||
text =
|
||||
"If the provider emits an event, the value of the client's provider status MUST be updated accordingly.")
|
||||
@Test
|
||||
void shouldSetTheStateToErrorWhenAnErrorEventIsEmitted() {
|
||||
assertThat(wrapper.getState()).isEqualTo(ProviderState.NOT_READY);
|
||||
wrapper.onEmit(
|
||||
ProviderEvent.PROVIDER_ERROR,
|
||||
ProviderEventDetails.builder().errorCode(ErrorCode.GENERAL).build()
|
||||
);
|
||||
ProviderEventDetails.builder().errorCode(ErrorCode.GENERAL).build());
|
||||
assertThat(wrapper.getState()).isEqualTo(ProviderState.ERROR);
|
||||
}
|
||||
|
||||
@Specification(number = "5.3.5", text = "If the provider emits an event, the value of the client's provider status MUST be updated accordingly.")
|
||||
@Specification(
|
||||
number = "5.3.5",
|
||||
text =
|
||||
"If the provider emits an event, the value of the client's provider status MUST be updated accordingly.")
|
||||
@Test
|
||||
void shouldSetTheStateToFatalWhenAFatalErrorEventIsEmitted() {
|
||||
assertThat(wrapper.getState()).isEqualTo(ProviderState.NOT_READY);
|
||||
wrapper.onEmit(
|
||||
ProviderEvent.PROVIDER_ERROR,
|
||||
ProviderEventDetails.builder().errorCode(ErrorCode.PROVIDER_FATAL).build()
|
||||
);
|
||||
ProviderEventDetails.builder()
|
||||
.errorCode(ErrorCode.PROVIDER_FATAL)
|
||||
.build());
|
||||
assertThat(wrapper.getState()).isEqualTo(ProviderState.FATAL);
|
||||
}
|
||||
|
||||
|
|
@ -141,7 +164,8 @@ class FeatureProviderStateManagerTest {
|
|||
}
|
||||
|
||||
@Override
|
||||
public ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) {
|
||||
public ProviderEvaluation<Boolean> getBooleanEvaluation(
|
||||
String key, Boolean defaultValue, EvaluationContext ctx) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -151,7 +175,8 @@ class FeatureProviderStateManagerTest {
|
|||
}
|
||||
|
||||
@Override
|
||||
public ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) {
|
||||
public ProviderEvaluation<Integer> getIntegerEvaluation(
|
||||
String key, Integer defaultValue, EvaluationContext ctx) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -178,4 +203,4 @@ class FeatureProviderStateManagerTest {
|
|||
shutdownCalled.incrementAndGet();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,13 +29,7 @@ class FlagEvaluationDetailsTest {
|
|||
ImmutableMetadata metadata = ImmutableMetadata.builder().build();
|
||||
|
||||
FlagEvaluationDetails<Integer> details = new FlagEvaluationDetails<>(
|
||||
flagKey,
|
||||
value,
|
||||
variant,
|
||||
reason.toString(),
|
||||
errorCode,
|
||||
errorMessage,
|
||||
metadata);
|
||||
flagKey, value, variant, reason.toString(), errorCode, errorMessage, metadata);
|
||||
|
||||
assertEquals(flagKey, details.getFlagKey());
|
||||
assertEquals(value, details.getValue());
|
||||
|
|
@ -48,13 +42,14 @@ class FlagEvaluationDetailsTest {
|
|||
|
||||
@Test
|
||||
@DisplayName("should be able to compare 2 FlagEvaluationDetails")
|
||||
public void compareFlagEvaluationDetails(){
|
||||
public void compareFlagEvaluationDetails() {
|
||||
FlagEvaluationDetails fed1 = FlagEvaluationDetails.builder()
|
||||
.reason(Reason.ERROR.toString())
|
||||
.value(false)
|
||||
.errorCode(ErrorCode.GENERAL)
|
||||
.errorMessage("error XXX")
|
||||
.flagMetadata(ImmutableMetadata.builder().addString("metadata","1").build())
|
||||
.flagMetadata(
|
||||
ImmutableMetadata.builder().addString("metadata", "1").build())
|
||||
.build();
|
||||
|
||||
FlagEvaluationDetails fed2 = FlagEvaluationDetails.builder()
|
||||
|
|
@ -62,9 +57,10 @@ class FlagEvaluationDetailsTest {
|
|||
.value(false)
|
||||
.errorCode(ErrorCode.GENERAL)
|
||||
.errorMessage("error XXX")
|
||||
.flagMetadata(ImmutableMetadata.builder().addString("metadata","1").build())
|
||||
.flagMetadata(
|
||||
ImmutableMetadata.builder().addString("metadata", "1").build())
|
||||
.build();
|
||||
|
||||
assertEquals(fed1,fed2);
|
||||
assertEquals(fed1, fed2);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,5 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import dev.openfeature.sdk.exceptions.GeneralError;
|
||||
import dev.openfeature.sdk.fixtures.HookFixtures;
|
||||
import dev.openfeature.sdk.providers.memory.InMemoryProvider;
|
||||
import dev.openfeature.sdk.testutils.FeatureProviderTestUtils;
|
||||
import dev.openfeature.sdk.testutils.TestEventsProvider;
|
||||
import lombok.SneakyThrows;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.simplify4u.slf4jmock.LoggerMock;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static dev.openfeature.sdk.DoSomethingProvider.DEFAULT_METADATA;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
|
@ -25,6 +7,22 @@ import static org.mockito.ArgumentMatchers.any;
|
|||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import dev.openfeature.sdk.exceptions.GeneralError;
|
||||
import dev.openfeature.sdk.fixtures.HookFixtures;
|
||||
import dev.openfeature.sdk.testutils.FeatureProviderTestUtils;
|
||||
import dev.openfeature.sdk.testutils.TestEventsProvider;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import lombok.SneakyThrows;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.simplify4u.slf4jmock.LoggerMock;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
class FlagEvaluationSpecTest implements HookFixtures {
|
||||
|
||||
private Logger logger;
|
||||
|
|
@ -48,34 +46,49 @@ class FlagEvaluationSpecTest implements HookFixtures {
|
|||
api = OpenFeatureAPI.getInstance();
|
||||
}
|
||||
|
||||
@AfterEach void reset_ctx() {
|
||||
@AfterEach
|
||||
void reset_ctx() {
|
||||
api.setEvaluationContext(null);
|
||||
}
|
||||
|
||||
@BeforeEach void set_logger() {
|
||||
@BeforeEach
|
||||
void set_logger() {
|
||||
logger = Mockito.mock(Logger.class);
|
||||
LoggerMock.setMock(OpenFeatureClient.class, logger);
|
||||
}
|
||||
|
||||
@AfterEach void reset_logs() {
|
||||
@AfterEach
|
||||
void reset_logs() {
|
||||
LoggerMock.setMock(OpenFeatureClient.class, logger);
|
||||
}
|
||||
|
||||
@Specification(number="1.1.1", text="The API, and any state it maintains SHOULD exist as a global singleton, even in cases wherein multiple versions of the API are present at runtime.")
|
||||
@Test void global_singleton() {
|
||||
@Specification(
|
||||
number = "1.1.1",
|
||||
text =
|
||||
"The API, and any state it maintains SHOULD exist as a global singleton, even in cases wherein multiple versions of the API are present at runtime.")
|
||||
@Test
|
||||
void global_singleton() {
|
||||
assertSame(OpenFeatureAPI.getInstance(), OpenFeatureAPI.getInstance());
|
||||
}
|
||||
|
||||
@Specification(number="1.1.2.1", text="The API MUST define a provider mutator, a function to set the default provider, which accepts an API-conformant provider implementation.")
|
||||
@Test void provider() {
|
||||
@Specification(
|
||||
number = "1.1.2.1",
|
||||
text =
|
||||
"The API MUST define a provider mutator, a function to set the default provider, which accepts an API-conformant provider implementation.")
|
||||
@Test
|
||||
void provider() {
|
||||
FeatureProvider mockProvider = mock(FeatureProvider.class);
|
||||
FeatureProviderTestUtils.setFeatureProvider(mockProvider);
|
||||
assertThat(api.getProvider()).isEqualTo(mockProvider);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
@Specification(number="1.1.8", text="The API SHOULD provide functions to set a provider and wait for the initialize function to return or throw.")
|
||||
@Test void providerAndWait() {
|
||||
@Specification(
|
||||
number = "1.1.8",
|
||||
text =
|
||||
"The API SHOULD provide functions to set a provider and wait for the initialize function to return or throw.")
|
||||
@Test
|
||||
void providerAndWait() {
|
||||
FeatureProvider provider = new TestEventsProvider(500);
|
||||
OpenFeatureAPI.getInstance().setProviderAndWait(provider);
|
||||
Client client = api.getClient();
|
||||
|
|
@ -89,8 +102,12 @@ class FlagEvaluationSpecTest implements HookFixtures {
|
|||
}
|
||||
|
||||
@SneakyThrows
|
||||
@Specification(number="1.1.8", text="The API SHOULD provide functions to set a provider and wait for the initialize function to return or throw.")
|
||||
@Test void providerAndWaitError() {
|
||||
@Specification(
|
||||
number = "1.1.8",
|
||||
text =
|
||||
"The API SHOULD provide functions to set a provider and wait for the initialize function to return or throw.")
|
||||
@Test
|
||||
void providerAndWaitError() {
|
||||
FeatureProvider provider1 = new TestEventsProvider(500, true, "fake error");
|
||||
assertThrows(GeneralError.class, () -> api.setProviderAndWait(provider1));
|
||||
|
||||
|
|
@ -99,8 +116,12 @@ class FlagEvaluationSpecTest implements HookFixtures {
|
|||
assertThrows(GeneralError.class, () -> api.setProviderAndWait(providerName, provider2));
|
||||
}
|
||||
|
||||
@Specification(number="2.4.5", text="The provider SHOULD indicate an error if flag resolution is attempted before the provider is ready.")
|
||||
@Test void shouldReturnNotReadyIfNotInitialized() {
|
||||
@Specification(
|
||||
number = "2.4.5",
|
||||
text =
|
||||
"The provider SHOULD indicate an error if flag resolution is attempted before the provider is ready.")
|
||||
@Test
|
||||
void shouldReturnNotReadyIfNotInitialized() {
|
||||
FeatureProvider provider = new TestEventsProvider(100);
|
||||
String providerName = "shouldReturnNotReadyIfNotInitialized";
|
||||
OpenFeatureAPI.getInstance().setProvider(providerName, provider);
|
||||
|
|
@ -110,14 +131,21 @@ class FlagEvaluationSpecTest implements HookFixtures {
|
|||
assertEquals(Reason.ERROR.toString(), details.getReason());
|
||||
}
|
||||
|
||||
@Specification(number="1.1.5", text="The API MUST provide a function for retrieving the metadata field of the configured provider.")
|
||||
@Test void provider_metadata() {
|
||||
@Specification(
|
||||
number = "1.1.5",
|
||||
text = "The API MUST provide a function for retrieving the metadata field of the configured provider.")
|
||||
@Test
|
||||
void provider_metadata() {
|
||||
FeatureProviderTestUtils.setFeatureProvider(new DoSomethingProvider());
|
||||
assertThat(api.getProviderMetadata().getName()).isEqualTo(DoSomethingProvider.name);
|
||||
}
|
||||
|
||||
@Specification(number="1.1.4", text="The API MUST provide a function to add hooks which accepts one or more API-conformant hooks, and appends them to the collection of any previously added hooks. When new hooks are added, previously added hooks are not removed.")
|
||||
@Test void hook_addition() {
|
||||
@Specification(
|
||||
number = "1.1.4",
|
||||
text =
|
||||
"The API MUST provide a function to add hooks which accepts one or more API-conformant hooks, and appends them to the collection of any previously added hooks. When new hooks are added, previously added hooks are not removed.")
|
||||
@Test
|
||||
void hook_addition() {
|
||||
Hook h1 = mock(Hook.class);
|
||||
Hook h2 = mock(Hook.class);
|
||||
api.addHooks(h1);
|
||||
|
|
@ -130,8 +158,12 @@ class FlagEvaluationSpecTest implements HookFixtures {
|
|||
assertEquals(h2, api.getHooks().get(1));
|
||||
}
|
||||
|
||||
@Specification(number="1.1.6", text="The API MUST provide a function for creating a client which accepts the following options: - domain (optional): A logical string identifier for binding clients to provider.")
|
||||
@Test void domainName() {
|
||||
@Specification(
|
||||
number = "1.1.6",
|
||||
text =
|
||||
"The API MUST provide a function for creating a client which accepts the following options: - domain (optional): A logical string identifier for binding clients to provider.")
|
||||
@Test
|
||||
void domainName() {
|
||||
assertNull(api.getClient().getMetadata().getDomain());
|
||||
|
||||
String domain = "Sir Calls-a-lot";
|
||||
|
|
@ -139,8 +171,12 @@ class FlagEvaluationSpecTest implements HookFixtures {
|
|||
assertEquals(domain, clientForDomain.getMetadata().getDomain());
|
||||
}
|
||||
|
||||
@Specification(number="1.2.1", text="The client MUST provide a method to add hooks which accepts one or more API-conformant hooks, and appends them to the collection of any previously added hooks. When new hooks are added, previously added hooks are not removed.")
|
||||
@Test void hookRegistration() {
|
||||
@Specification(
|
||||
number = "1.2.1",
|
||||
text =
|
||||
"The client MUST provide a method to add hooks which accepts one or more API-conformant hooks, and appends them to the collection of any previously added hooks. When new hooks are added, previously added hooks are not removed.")
|
||||
@Test
|
||||
void hookRegistration() {
|
||||
Client c = _client();
|
||||
Hook m1 = mock(Hook.class);
|
||||
Hook m2 = mock(Hook.class);
|
||||
|
|
@ -152,9 +188,16 @@ class FlagEvaluationSpecTest implements HookFixtures {
|
|||
assertTrue(hooks.contains(m2));
|
||||
}
|
||||
|
||||
@Specification(number="1.3.1.1", text="The client MUST provide methods for typed flag evaluation, including boolean, numeric, string, and structure, with parameters flag key (string, required), default value (boolean | number | string | structure, required), evaluation context (optional), and evaluation options (optional), which returns the flag value.")
|
||||
@Specification(number="1.3.3.1", text="The client SHOULD provide functions for floating-point numbers and integers, consistent with language idioms.")
|
||||
@Test void value_flags() {
|
||||
@Specification(
|
||||
number = "1.3.1.1",
|
||||
text =
|
||||
"The client MUST provide methods for typed flag evaluation, including boolean, numeric, string, and structure, with parameters flag key (string, required), default value (boolean | number | string | structure, required), evaluation context (optional), and evaluation options (optional), which returns the flag value.")
|
||||
@Specification(
|
||||
number = "1.3.3.1",
|
||||
text =
|
||||
"The client SHOULD provide functions for floating-point numbers and integers, consistent with language idioms.")
|
||||
@Test
|
||||
void value_flags() {
|
||||
FeatureProviderTestUtils.setFeatureProvider(new DoSomethingProvider());
|
||||
|
||||
Client c = api.getClient();
|
||||
|
|
@ -162,32 +205,80 @@ class FlagEvaluationSpecTest implements HookFixtures {
|
|||
|
||||
assertEquals(true, c.getBooleanValue(key, false));
|
||||
assertEquals(true, c.getBooleanValue(key, false, new ImmutableContext()));
|
||||
assertEquals(true, c.getBooleanValue(key, false, new ImmutableContext(), FlagEvaluationOptions.builder().build()));
|
||||
assertEquals(
|
||||
true,
|
||||
c.getBooleanValue(
|
||||
key,
|
||||
false,
|
||||
new ImmutableContext(),
|
||||
FlagEvaluationOptions.builder().build()));
|
||||
|
||||
assertEquals("gnirts-ym", c.getStringValue(key, "my-string"));
|
||||
assertEquals("gnirts-ym", c.getStringValue(key, "my-string", new ImmutableContext()));
|
||||
assertEquals("gnirts-ym", c.getStringValue(key, "my-string", new ImmutableContext(), FlagEvaluationOptions.builder().build()));
|
||||
assertEquals(
|
||||
"gnirts-ym",
|
||||
c.getStringValue(
|
||||
key,
|
||||
"my-string",
|
||||
new ImmutableContext(),
|
||||
FlagEvaluationOptions.builder().build()));
|
||||
|
||||
assertEquals(400, c.getIntegerValue(key, 4));
|
||||
assertEquals(400, c.getIntegerValue(key, 4, new ImmutableContext()));
|
||||
assertEquals(400, c.getIntegerValue(key, 4, new ImmutableContext(), FlagEvaluationOptions.builder().build()));
|
||||
assertEquals(
|
||||
400,
|
||||
c.getIntegerValue(
|
||||
key,
|
||||
4,
|
||||
new ImmutableContext(),
|
||||
FlagEvaluationOptions.builder().build()));
|
||||
|
||||
assertEquals(40.0, c.getDoubleValue(key, .4));
|
||||
assertEquals(40.0, c.getDoubleValue(key, .4, new ImmutableContext()));
|
||||
assertEquals(40.0, c.getDoubleValue(key, .4, new ImmutableContext(), FlagEvaluationOptions.builder().build()));
|
||||
assertEquals(
|
||||
40.0,
|
||||
c.getDoubleValue(
|
||||
key,
|
||||
.4,
|
||||
new ImmutableContext(),
|
||||
FlagEvaluationOptions.builder().build()));
|
||||
|
||||
assertEquals(null, c.getObjectValue(key, new Value()));
|
||||
assertEquals(null, c.getObjectValue(key, new Value(), new ImmutableContext()));
|
||||
assertEquals(null, c.getObjectValue(key, new Value(), new ImmutableContext(), FlagEvaluationOptions.builder().build()));
|
||||
assertEquals(
|
||||
null,
|
||||
c.getObjectValue(
|
||||
key,
|
||||
new Value(),
|
||||
new ImmutableContext(),
|
||||
FlagEvaluationOptions.builder().build()));
|
||||
}
|
||||
|
||||
@Specification(number="1.4.1.1", text="The client MUST provide methods for detailed flag value evaluation with parameters flag key (string, required), default value (boolean | number | string | structure, required), evaluation context (optional), and evaluation options (optional), which returns an evaluation details structure.")
|
||||
@Specification(number="1.4.3", text="The evaluation details structure's value field MUST contain the evaluated flag value.")
|
||||
@Specification(number="1.4.4.1", text="The evaluation details structure SHOULD accept a generic argument (or use an equivalent language feature) which indicates the type of the wrapped value field.")
|
||||
@Specification(number="1.4.5", text="The evaluation details structure's flag key field MUST contain the flag key argument passed to the detailed flag evaluation method.")
|
||||
@Specification(number="1.4.6", text="In cases of normal execution, the evaluation details structure's variant field MUST contain the value of the variant field in the flag resolution structure returned by the configured provider, if the field is set.")
|
||||
@Specification(number="1.4.7", text="In cases of normal execution, the `evaluation details` structure's `reason` field MUST contain the value of the `reason` field in the `flag resolution` structure returned by the configured `provider`, if the field is set.")
|
||||
@Test void detail_flags() {
|
||||
@Specification(
|
||||
number = "1.4.1.1",
|
||||
text =
|
||||
"The client MUST provide methods for detailed flag value evaluation with parameters flag key (string, required), default value (boolean | number | string | structure, required), evaluation context (optional), and evaluation options (optional), which returns an evaluation details structure.")
|
||||
@Specification(
|
||||
number = "1.4.3",
|
||||
text = "The evaluation details structure's value field MUST contain the evaluated flag value.")
|
||||
@Specification(
|
||||
number = "1.4.4.1",
|
||||
text =
|
||||
"The evaluation details structure SHOULD accept a generic argument (or use an equivalent language feature) which indicates the type of the wrapped value field.")
|
||||
@Specification(
|
||||
number = "1.4.5",
|
||||
text =
|
||||
"The evaluation details structure's flag key field MUST contain the flag key argument passed to the detailed flag evaluation method.")
|
||||
@Specification(
|
||||
number = "1.4.6",
|
||||
text =
|
||||
"In cases of normal execution, the evaluation details structure's variant field MUST contain the value of the variant field in the flag resolution structure returned by the configured provider, if the field is set.")
|
||||
@Specification(
|
||||
number = "1.4.7",
|
||||
text =
|
||||
"In cases of normal execution, the `evaluation details` structure's `reason` field MUST contain the value of the `reason` field in the `flag resolution` structure returned by the configured `provider`, if the field is set.")
|
||||
@Test
|
||||
void detail_flags() {
|
||||
FeatureProviderTestUtils.setFeatureProvider(new DoSomethingProvider());
|
||||
Client c = api.getClient();
|
||||
String key = "key";
|
||||
|
|
@ -200,7 +291,13 @@ class FlagEvaluationSpecTest implements HookFixtures {
|
|||
.build();
|
||||
assertEquals(bd, c.getBooleanDetails(key, true));
|
||||
assertEquals(bd, c.getBooleanDetails(key, true, new ImmutableContext()));
|
||||
assertEquals(bd, c.getBooleanDetails(key, true, new ImmutableContext(), FlagEvaluationOptions.builder().build()));
|
||||
assertEquals(
|
||||
bd,
|
||||
c.getBooleanDetails(
|
||||
key,
|
||||
true,
|
||||
new ImmutableContext(),
|
||||
FlagEvaluationOptions.builder().build()));
|
||||
|
||||
FlagEvaluationDetails<String> sd = FlagEvaluationDetails.<String>builder()
|
||||
.flagKey(key)
|
||||
|
|
@ -210,7 +307,13 @@ class FlagEvaluationSpecTest implements HookFixtures {
|
|||
.build();
|
||||
assertEquals(sd, c.getStringDetails(key, "test"));
|
||||
assertEquals(sd, c.getStringDetails(key, "test", new ImmutableContext()));
|
||||
assertEquals(sd, c.getStringDetails(key, "test", new ImmutableContext(), FlagEvaluationOptions.builder().build()));
|
||||
assertEquals(
|
||||
sd,
|
||||
c.getStringDetails(
|
||||
key,
|
||||
"test",
|
||||
new ImmutableContext(),
|
||||
FlagEvaluationOptions.builder().build()));
|
||||
|
||||
FlagEvaluationDetails<Integer> id = FlagEvaluationDetails.<Integer>builder()
|
||||
.flagKey(key)
|
||||
|
|
@ -219,7 +322,13 @@ class FlagEvaluationSpecTest implements HookFixtures {
|
|||
.build();
|
||||
assertEquals(id, c.getIntegerDetails(key, 4));
|
||||
assertEquals(id, c.getIntegerDetails(key, 4, new ImmutableContext()));
|
||||
assertEquals(id, c.getIntegerDetails(key, 4, new ImmutableContext(), FlagEvaluationOptions.builder().build()));
|
||||
assertEquals(
|
||||
id,
|
||||
c.getIntegerDetails(
|
||||
key,
|
||||
4,
|
||||
new ImmutableContext(),
|
||||
FlagEvaluationOptions.builder().build()));
|
||||
|
||||
FlagEvaluationDetails<Double> dd = FlagEvaluationDetails.<Double>builder()
|
||||
.flagKey(key)
|
||||
|
|
@ -228,30 +337,55 @@ class FlagEvaluationSpecTest implements HookFixtures {
|
|||
.build();
|
||||
assertEquals(dd, c.getDoubleDetails(key, .4));
|
||||
assertEquals(dd, c.getDoubleDetails(key, .4, new ImmutableContext()));
|
||||
assertEquals(dd, c.getDoubleDetails(key, .4, new ImmutableContext(), FlagEvaluationOptions.builder().build()));
|
||||
assertEquals(
|
||||
dd,
|
||||
c.getDoubleDetails(
|
||||
key,
|
||||
.4,
|
||||
new ImmutableContext(),
|
||||
FlagEvaluationOptions.builder().build()));
|
||||
|
||||
// TODO: Structure detail tests.
|
||||
}
|
||||
|
||||
@Specification(number="1.5.1", text="The evaluation options structure's hooks field denotes an ordered collection of hooks that the client MUST execute for the respective flag evaluation, in addition to those already configured.")
|
||||
@Specification(
|
||||
number = "1.5.1",
|
||||
text =
|
||||
"The evaluation options structure's hooks field denotes an ordered collection of hooks that the client MUST execute for the respective flag evaluation, in addition to those already configured.")
|
||||
@SneakyThrows
|
||||
@Test void hooks() {
|
||||
@Test
|
||||
void hooks() {
|
||||
Client c = _initializedClient();
|
||||
Hook<Boolean> clientHook = mockBooleanHook();
|
||||
Hook<Boolean> invocationHook = mockBooleanHook();
|
||||
c.addHooks(clientHook);
|
||||
c.getBooleanValue("key", false, null, FlagEvaluationOptions.builder()
|
||||
.hook(invocationHook)
|
||||
.build());
|
||||
c.getBooleanValue(
|
||||
"key",
|
||||
false,
|
||||
null,
|
||||
FlagEvaluationOptions.builder().hook(invocationHook).build());
|
||||
verify(clientHook, times(1)).before(any(), any());
|
||||
verify(invocationHook, times(1)).before(any(), any());
|
||||
}
|
||||
|
||||
@Specification(number="1.4.8", text="In cases of abnormal execution, the `evaluation details` structure's `error code` field **MUST** contain an `error code`.")
|
||||
@Specification(number="1.4.9", text="In cases of abnormal execution (network failure, unhandled error, etc) the `reason` field in the `evaluation details` SHOULD indicate an error.")
|
||||
@Specification(number="1.4.10", text="Methods, functions, or operations on the client MUST NOT throw exceptions, or otherwise abnormally terminate. Flag evaluation calls must always return the `default value` in the event of abnormal execution. Exceptions include functions or methods for the purposes for configuration or setup.")
|
||||
@Specification(number="1.4.13", text="In cases of abnormal execution, the `evaluation details` structure's `error message` field **MAY** contain a string containing additional details about the nature of the error.")
|
||||
@Test void broken_provider() {
|
||||
@Specification(
|
||||
number = "1.4.8",
|
||||
text =
|
||||
"In cases of abnormal execution, the `evaluation details` structure's `error code` field **MUST** contain an `error code`.")
|
||||
@Specification(
|
||||
number = "1.4.9",
|
||||
text =
|
||||
"In cases of abnormal execution (network failure, unhandled error, etc) the `reason` field in the `evaluation details` SHOULD indicate an error.")
|
||||
@Specification(
|
||||
number = "1.4.10",
|
||||
text =
|
||||
"Methods, functions, or operations on the client MUST NOT throw exceptions, or otherwise abnormally terminate. Flag evaluation calls must always return the `default value` in the event of abnormal execution. Exceptions include functions or methods for the purposes for configuration or setup.")
|
||||
@Specification(
|
||||
number = "1.4.13",
|
||||
text =
|
||||
"In cases of abnormal execution, the `evaluation details` structure's `error message` field **MAY** contain a string containing additional details about the nature of the error.")
|
||||
@Test
|
||||
void broken_provider() {
|
||||
FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenProvider());
|
||||
Client c = api.getClient();
|
||||
boolean defaultValue = false;
|
||||
|
|
@ -263,11 +397,24 @@ class FlagEvaluationSpecTest implements HookFixtures {
|
|||
assertEquals(defaultValue, details.getValue());
|
||||
}
|
||||
|
||||
@Specification(number="1.4.8", text="In cases of abnormal execution, the `evaluation details` structure's `error code` field **MUST** contain an `error code`.")
|
||||
@Specification(number="1.4.9", text="In cases of abnormal execution (network failure, unhandled error, etc) the `reason` field in the `evaluation details` SHOULD indicate an error.")
|
||||
@Specification(number="1.4.10", text="Methods, functions, or operations on the client MUST NOT throw exceptions, or otherwise abnormally terminate. Flag evaluation calls must always return the `default value` in the event of abnormal execution. Exceptions include functions or methods for the purposes for configuration or setup.")
|
||||
@Specification(number="1.4.13", text="In cases of abnormal execution, the `evaluation details` structure's `error message` field **MAY** contain a string containing additional details about the nature of the error.")
|
||||
@Test void broken_provider_withDetails() {
|
||||
@Specification(
|
||||
number = "1.4.8",
|
||||
text =
|
||||
"In cases of abnormal execution, the `evaluation details` structure's `error code` field **MUST** contain an `error code`.")
|
||||
@Specification(
|
||||
number = "1.4.9",
|
||||
text =
|
||||
"In cases of abnormal execution (network failure, unhandled error, etc) the `reason` field in the `evaluation details` SHOULD indicate an error.")
|
||||
@Specification(
|
||||
number = "1.4.10",
|
||||
text =
|
||||
"Methods, functions, or operations on the client MUST NOT throw exceptions, or otherwise abnormally terminate. Flag evaluation calls must always return the `default value` in the event of abnormal execution. Exceptions include functions or methods for the purposes for configuration or setup.")
|
||||
@Specification(
|
||||
number = "1.4.13",
|
||||
text =
|
||||
"In cases of abnormal execution, the `evaluation details` structure's `error message` field **MAY** contain a string containing additional details about the nature of the error.")
|
||||
@Test
|
||||
void broken_provider_withDetails() {
|
||||
FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenWithDetailsProvider());
|
||||
Client c = api.getClient();
|
||||
boolean defaultValue = false;
|
||||
|
|
@ -279,21 +426,25 @@ class FlagEvaluationSpecTest implements HookFixtures {
|
|||
assertEquals(defaultValue, details.getValue());
|
||||
}
|
||||
|
||||
@Specification(number="1.4.11", text="Methods, functions, or operations on the client SHOULD NOT write log messages.")
|
||||
@Test void log_on_error() throws NotImplementedException {
|
||||
@Specification(
|
||||
number = "1.4.11",
|
||||
text = "Methods, functions, or operations on the client SHOULD NOT write log messages.")
|
||||
@Test
|
||||
void log_on_error() throws NotImplementedException {
|
||||
FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenProvider());
|
||||
Client c = api.getClient();
|
||||
FlagEvaluationDetails<Boolean> result = c.getBooleanDetails("test", false);
|
||||
|
||||
assertEquals(Reason.ERROR.toString(), result.getReason());
|
||||
Mockito.verify(logger, never()).error(
|
||||
any(String.class),
|
||||
any(),
|
||||
any());
|
||||
Mockito.verify(logger, never()).error(any(String.class), any(), any());
|
||||
}
|
||||
|
||||
@Specification(number="1.2.2", text="The client interface MUST define a metadata member or accessor, containing an immutable domain field or accessor of type string, which corresponds to the domain value supplied during client creation. In previous drafts, this property was called name. For backwards compatibility, implementations should consider name an alias to domain.")
|
||||
@Test void clientMetadata() {
|
||||
@Specification(
|
||||
number = "1.2.2",
|
||||
text =
|
||||
"The client interface MUST define a metadata member or accessor, containing an immutable domain field or accessor of type string, which corresponds to the domain value supplied during client creation. In previous drafts, this property was called name. For backwards compatibility, implementations should consider name an alias to domain.")
|
||||
@Test
|
||||
void clientMetadata() {
|
||||
Client c = _client();
|
||||
assertNull(c.getMetadata().getName());
|
||||
assertNull(c.getMetadata().getDomain());
|
||||
|
|
@ -306,27 +457,36 @@ class FlagEvaluationSpecTest implements HookFixtures {
|
|||
assertEquals(domainName, c2.getMetadata().getDomain());
|
||||
}
|
||||
|
||||
@Specification(number="1.4.9", text="In cases of abnormal execution (network failure, unhandled error, etc) the reason field in the evaluation details SHOULD indicate an error.")
|
||||
@Test void reason_is_error_when_there_are_errors() {
|
||||
@Specification(
|
||||
number = "1.4.9",
|
||||
text =
|
||||
"In cases of abnormal execution (network failure, unhandled error, etc) the reason field in the evaluation details SHOULD indicate an error.")
|
||||
@Test
|
||||
void reason_is_error_when_there_are_errors() {
|
||||
FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenProvider());
|
||||
Client c = api.getClient();
|
||||
FlagEvaluationDetails<Boolean> result = c.getBooleanDetails("test", false);
|
||||
assertEquals(Reason.ERROR.toString(), result.getReason());
|
||||
}
|
||||
|
||||
@Specification(number="1.4.14", text="If the flag metadata field in the flag resolution structure returned by the configured provider is set, the evaluation details structure's flag metadata field MUST contain that value. Otherwise, it MUST contain an empty record.")
|
||||
@Test void flag_metadata_passed() {
|
||||
@Specification(
|
||||
number = "1.4.14",
|
||||
text =
|
||||
"If the flag metadata field in the flag resolution structure returned by the configured provider is set, the evaluation details structure's flag metadata field MUST contain that value. Otherwise, it MUST contain an empty record.")
|
||||
@Test
|
||||
void flag_metadata_passed() {
|
||||
FeatureProviderTestUtils.setFeatureProvider(new DoSomethingProvider(null));
|
||||
Client c = api.getClient();
|
||||
FlagEvaluationDetails<Boolean> result = c.getBooleanDetails("test", false);
|
||||
assertNotNull(result.getFlagMetadata());
|
||||
}
|
||||
|
||||
@Specification(number="3.2.2.1", text="The API MUST have a method for setting the global evaluation context.")
|
||||
@Test void api_context() {
|
||||
@Specification(number = "3.2.2.1", text = "The API MUST have a method for setting the global evaluation context.")
|
||||
@Test
|
||||
void api_context() {
|
||||
String contextKey = "some-key";
|
||||
String contextValue = "some-value";
|
||||
DoSomethingProvider provider = spy( new DoSomethingProvider());
|
||||
DoSomethingProvider provider = spy(new DoSomethingProvider());
|
||||
FeatureProviderTestUtils.setFeatureProvider(provider);
|
||||
|
||||
Map<String, Value> attributes = new HashMap<>();
|
||||
|
|
@ -339,12 +499,20 @@ class FlagEvaluationSpecTest implements HookFixtures {
|
|||
client.getBooleanValue("any-flag", false);
|
||||
|
||||
// assert that the value from the global context was passed to the provider
|
||||
verify(provider).getBooleanEvaluation(any(), any(), argThat((arg) -> arg.getValue(contextKey).asString().equals(contextValue)));
|
||||
verify(provider).getBooleanEvaluation(any(), any(), argThat((arg) -> arg.getValue(contextKey)
|
||||
.asString()
|
||||
.equals(contextValue)));
|
||||
}
|
||||
|
||||
@Specification(number="3.2.1.1", text="The API, Client and invocation MUST have a method for supplying evaluation context.")
|
||||
@Specification(number="3.2.3", text="Evaluation context MUST be merged in the order: API (global; lowest precedence) -> transaction -> client -> invocation -> before hooks (highest precedence), with duplicate values being overwritten.")
|
||||
@Test void multi_layer_context_merges_correctly() {
|
||||
@Specification(
|
||||
number = "3.2.1.1",
|
||||
text = "The API, Client and invocation MUST have a method for supplying evaluation context.")
|
||||
@Specification(
|
||||
number = "3.2.3",
|
||||
text =
|
||||
"Evaluation context MUST be merged in the order: API (global; lowest precedence) -> transaction -> client -> invocation -> before hooks (highest precedence), with duplicate values being overwritten.")
|
||||
@Test
|
||||
void multi_layer_context_merges_correctly() {
|
||||
DoSomethingProvider provider = spy(new DoSomethingProvider());
|
||||
FeatureProviderTestUtils.setFeatureProvider(provider);
|
||||
TransactionContextPropagator transactionContextPropagator = new ThreadLocalTransactionContextPropagator();
|
||||
|
|
@ -357,8 +525,10 @@ class FlagEvaluationSpecTest implements HookFixtures {
|
|||
attrs.put("common7", new Value("5"));
|
||||
return Optional.ofNullable(new ImmutableContext(attrs));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void after(HookContext<Boolean> ctx, FlagEvaluationDetails<Boolean> details, Map<String, Object> hints) {
|
||||
public void after(
|
||||
HookContext<Boolean> ctx, FlagEvaluationDetails<Boolean> details, Map<String, Object> hints) {
|
||||
Hook.super.after(ctx, details, hints);
|
||||
}
|
||||
});
|
||||
|
|
@ -404,59 +574,133 @@ class FlagEvaluationSpecTest implements HookFixtures {
|
|||
invocationAttributes.put("invocation", new Value("4"));
|
||||
EvaluationContext invocationCtx = new ImmutableContext(invocationAttributes);
|
||||
|
||||
c.getBooleanValue("key", false, invocationCtx, FlagEvaluationOptions.builder().hook(hook).build());
|
||||
c.getBooleanValue(
|
||||
"key",
|
||||
false,
|
||||
invocationCtx,
|
||||
FlagEvaluationOptions.builder().hook(hook).build());
|
||||
|
||||
// assert the correct overrides in before hook
|
||||
verify(hook).before(argThat((arg) -> {
|
||||
EvaluationContext evaluationContext = arg.getCtx();
|
||||
return evaluationContext.getValue("api").asString().equals("1") &&
|
||||
evaluationContext.getValue("transaction").asString().equals("2") &&
|
||||
evaluationContext.getValue("client").asString().equals("3") &&
|
||||
evaluationContext.getValue("invocation").asString().equals("4") &&
|
||||
evaluationContext.getValue("common1").asString().equals("2") &&
|
||||
evaluationContext.getValue("common2").asString().equals("3") &&
|
||||
evaluationContext.getValue("common3").asString().equals("4") &&
|
||||
evaluationContext.getValue("common4").asString().equals("3") &&
|
||||
evaluationContext.getValue("common5").asString().equals("4") &&
|
||||
evaluationContext.getValue("common6").asString().equals("4");
|
||||
}), any());
|
||||
verify(hook)
|
||||
.before(
|
||||
argThat((arg) -> {
|
||||
EvaluationContext evaluationContext = arg.getCtx();
|
||||
return evaluationContext.getValue("api").asString().equals("1")
|
||||
&& evaluationContext
|
||||
.getValue("transaction")
|
||||
.asString()
|
||||
.equals("2")
|
||||
&& evaluationContext
|
||||
.getValue("client")
|
||||
.asString()
|
||||
.equals("3")
|
||||
&& evaluationContext
|
||||
.getValue("invocation")
|
||||
.asString()
|
||||
.equals("4")
|
||||
&& evaluationContext
|
||||
.getValue("common1")
|
||||
.asString()
|
||||
.equals("2")
|
||||
&& evaluationContext
|
||||
.getValue("common2")
|
||||
.asString()
|
||||
.equals("3")
|
||||
&& evaluationContext
|
||||
.getValue("common3")
|
||||
.asString()
|
||||
.equals("4")
|
||||
&& evaluationContext
|
||||
.getValue("common4")
|
||||
.asString()
|
||||
.equals("3")
|
||||
&& evaluationContext
|
||||
.getValue("common5")
|
||||
.asString()
|
||||
.equals("4")
|
||||
&& evaluationContext
|
||||
.getValue("common6")
|
||||
.asString()
|
||||
.equals("4");
|
||||
}),
|
||||
any());
|
||||
|
||||
// assert the correct overrides in evaluation
|
||||
verify(provider).getBooleanEvaluation(any(), any(), argThat((arg) -> {
|
||||
return arg.getValue("api").asString().equals("1") &&
|
||||
arg.getValue("transaction").asString().equals("2") &&
|
||||
arg.getValue("client").asString().equals("3") &&
|
||||
arg.getValue("invocation").asString().equals("4") &&
|
||||
arg.getValue("before").asString().equals("5") &&
|
||||
arg.getValue("common1").asString().equals("2") &&
|
||||
arg.getValue("common2").asString().equals("3") &&
|
||||
arg.getValue("common3").asString().equals("4") &&
|
||||
arg.getValue("common4").asString().equals("3") &&
|
||||
arg.getValue("common5").asString().equals("4") &&
|
||||
arg.getValue("common6").asString().equals("4") &&
|
||||
arg.getValue("common7").asString().equals("5");
|
||||
return arg.getValue("api").asString().equals("1")
|
||||
&& arg.getValue("transaction").asString().equals("2")
|
||||
&& arg.getValue("client").asString().equals("3")
|
||||
&& arg.getValue("invocation").asString().equals("4")
|
||||
&& arg.getValue("before").asString().equals("5")
|
||||
&& arg.getValue("common1").asString().equals("2")
|
||||
&& arg.getValue("common2").asString().equals("3")
|
||||
&& arg.getValue("common3").asString().equals("4")
|
||||
&& arg.getValue("common4").asString().equals("3")
|
||||
&& arg.getValue("common5").asString().equals("4")
|
||||
&& arg.getValue("common6").asString().equals("4")
|
||||
&& arg.getValue("common7").asString().equals("5");
|
||||
}));
|
||||
|
||||
// assert the correct overrides in after hook
|
||||
verify(hook).after(argThat((arg) -> {
|
||||
EvaluationContext evaluationContext = arg.getCtx();
|
||||
return evaluationContext.getValue("api").asString().equals("1") &&
|
||||
evaluationContext.getValue("transaction").asString().equals("2") &&
|
||||
evaluationContext.getValue("client").asString().equals("3") &&
|
||||
evaluationContext.getValue("invocation").asString().equals("4") &&
|
||||
evaluationContext.getValue("before").asString().equals("5") &&
|
||||
evaluationContext.getValue("common1").asString().equals("2") &&
|
||||
evaluationContext.getValue("common2").asString().equals("3") &&
|
||||
evaluationContext.getValue("common3").asString().equals("4") &&
|
||||
evaluationContext.getValue("common4").asString().equals("3") &&
|
||||
evaluationContext.getValue("common5").asString().equals("4") &&
|
||||
evaluationContext.getValue("common6").asString().equals("4") &&
|
||||
evaluationContext.getValue("common7").asString().equals("5");
|
||||
}), any(), any());
|
||||
verify(hook)
|
||||
.after(
|
||||
argThat((arg) -> {
|
||||
EvaluationContext evaluationContext = arg.getCtx();
|
||||
return evaluationContext.getValue("api").asString().equals("1")
|
||||
&& evaluationContext
|
||||
.getValue("transaction")
|
||||
.asString()
|
||||
.equals("2")
|
||||
&& evaluationContext
|
||||
.getValue("client")
|
||||
.asString()
|
||||
.equals("3")
|
||||
&& evaluationContext
|
||||
.getValue("invocation")
|
||||
.asString()
|
||||
.equals("4")
|
||||
&& evaluationContext
|
||||
.getValue("before")
|
||||
.asString()
|
||||
.equals("5")
|
||||
&& evaluationContext
|
||||
.getValue("common1")
|
||||
.asString()
|
||||
.equals("2")
|
||||
&& evaluationContext
|
||||
.getValue("common2")
|
||||
.asString()
|
||||
.equals("3")
|
||||
&& evaluationContext
|
||||
.getValue("common3")
|
||||
.asString()
|
||||
.equals("4")
|
||||
&& evaluationContext
|
||||
.getValue("common4")
|
||||
.asString()
|
||||
.equals("3")
|
||||
&& evaluationContext
|
||||
.getValue("common5")
|
||||
.asString()
|
||||
.equals("4")
|
||||
&& evaluationContext
|
||||
.getValue("common6")
|
||||
.asString()
|
||||
.equals("4")
|
||||
&& evaluationContext
|
||||
.getValue("common7")
|
||||
.asString()
|
||||
.equals("5");
|
||||
}),
|
||||
any(),
|
||||
any());
|
||||
}
|
||||
|
||||
@Specification(number="3.3.1.1", text="The API SHOULD have a method for setting a transaction context propagator.")
|
||||
@Test void setting_transaction_context_propagator() {
|
||||
@Specification(
|
||||
number = "3.3.1.1",
|
||||
text = "The API SHOULD have a method for setting a transaction context propagator.")
|
||||
@Test
|
||||
void setting_transaction_context_propagator() {
|
||||
DoSomethingProvider provider = new DoSomethingProvider();
|
||||
FeatureProviderTestUtils.setFeatureProvider(provider);
|
||||
|
||||
|
|
@ -465,8 +709,12 @@ class FlagEvaluationSpecTest implements HookFixtures {
|
|||
assertEquals(transactionContextPropagator, api.getTransactionContextPropagator());
|
||||
}
|
||||
|
||||
@Specification(number="3.3.1.2.1", text="The API MUST have a method for setting the evaluation context of the transaction context propagator for the current transaction.")
|
||||
@Test void setting_transaction_context() {
|
||||
@Specification(
|
||||
number = "3.3.1.2.1",
|
||||
text =
|
||||
"The API MUST have a method for setting the evaluation context of the transaction context propagator for the current transaction.")
|
||||
@Test
|
||||
void setting_transaction_context() {
|
||||
DoSomethingProvider provider = new DoSomethingProvider();
|
||||
FeatureProviderTestUtils.setFeatureProvider(provider);
|
||||
|
||||
|
|
@ -481,9 +729,16 @@ class FlagEvaluationSpecTest implements HookFixtures {
|
|||
assertEquals(transactionContext, transactionContextPropagator.getTransactionContext());
|
||||
}
|
||||
|
||||
@Specification(number="3.3.1.2.2", text="A transaction context propagator MUST have a method for setting the evaluation context of the current transaction.")
|
||||
@Specification(number="3.3.1.2.3", text="A transaction context propagator MUST have a method for getting the evaluation context of the current transaction.")
|
||||
@Test void transaction_context_propagator_setting_context() {
|
||||
@Specification(
|
||||
number = "3.3.1.2.2",
|
||||
text =
|
||||
"A transaction context propagator MUST have a method for setting the evaluation context of the current transaction.")
|
||||
@Specification(
|
||||
number = "3.3.1.2.3",
|
||||
text =
|
||||
"A transaction context propagator MUST have a method for getting the evaluation context of the current transaction.")
|
||||
@Test
|
||||
void transaction_context_propagator_setting_context() {
|
||||
TransactionContextPropagator transactionContextPropagator = new ThreadLocalTransactionContextPropagator();
|
||||
|
||||
Map<String, Value> attributes = new HashMap<>();
|
||||
|
|
@ -494,23 +749,46 @@ class FlagEvaluationSpecTest implements HookFixtures {
|
|||
assertEquals(transactionContext, transactionContextPropagator.getTransactionContext());
|
||||
}
|
||||
|
||||
@Specification(number="1.3.4", text="The client SHOULD guarantee the returned value of any typed flag evaluation method is of the expected type. If the value returned by the underlying provider implementation does not match the expected type, it's to be considered abnormal execution, and the supplied default value should be returned.")
|
||||
@Test void type_system_prevents_this() {}
|
||||
@Specification(
|
||||
number = "1.3.4",
|
||||
text =
|
||||
"The client SHOULD guarantee the returned value of any typed flag evaluation method is of the expected type. If the value returned by the underlying provider implementation does not match the expected type, it's to be considered abnormal execution, and the supplied default value should be returned.")
|
||||
@Test
|
||||
void type_system_prevents_this() {}
|
||||
|
||||
@Specification(number="1.1.7", text="The client creation function MUST NOT throw, or otherwise abnormally terminate.")
|
||||
@Test void constructor_does_not_throw() {}
|
||||
@Specification(
|
||||
number = "1.1.7",
|
||||
text = "The client creation function MUST NOT throw, or otherwise abnormally terminate.")
|
||||
@Test
|
||||
void constructor_does_not_throw() {}
|
||||
|
||||
@Specification(number="1.4.12", text="The client SHOULD provide asynchronous or non-blocking mechanisms for flag evaluation.")
|
||||
@Test void one_thread_per_request_model() {}
|
||||
@Specification(
|
||||
number = "1.4.12",
|
||||
text = "The client SHOULD provide asynchronous or non-blocking mechanisms for flag evaluation.")
|
||||
@Test
|
||||
void one_thread_per_request_model() {}
|
||||
|
||||
@Specification(number="1.4.14.1", text="Condition: Flag metadata MUST be immutable.")
|
||||
@Test void compiler_enforced() {}
|
||||
|
||||
@Specification(number="1.4.2.1", text="The client MUST provide methods for detailed flag value evaluation with parameters flag key (string, required), default value (boolean | number | string | structure, required), and evaluation options (optional), which returns an evaluation details structure.")
|
||||
@Specification(number="1.3.2.1", text="The client MUST provide methods for typed flag evaluation, including boolean, numeric, string, and structure, with parameters flag key (string, required), default value (boolean | number | string | structure, required), and evaluation options (optional), which returns the flag value.")
|
||||
@Specification(number="3.2.2.2", text="The Client and invocation MUST NOT have a method for supplying evaluation context.")
|
||||
@Specification(number="3.2.4.1", text="When the global evaluation context is set, the on context changed handler MUST run.")
|
||||
@Specification(number="3.3.2.1", text="The API MUST NOT have a method for setting a transaction context propagator.")
|
||||
@Test void not_applicable_for_dynamic_context() {}
|
||||
@Specification(number = "1.4.14.1", text = "Condition: Flag metadata MUST be immutable.")
|
||||
@Test
|
||||
void compiler_enforced() {}
|
||||
|
||||
@Specification(
|
||||
number = "1.4.2.1",
|
||||
text =
|
||||
"The client MUST provide methods for detailed flag value evaluation with parameters flag key (string, required), default value (boolean | number | string | structure, required), and evaluation options (optional), which returns an evaluation details structure.")
|
||||
@Specification(
|
||||
number = "1.3.2.1",
|
||||
text =
|
||||
"The client MUST provide methods for typed flag evaluation, including boolean, numeric, string, and structure, with parameters flag key (string, required), default value (boolean | number | string | structure, required), and evaluation options (optional), which returns the flag value.")
|
||||
@Specification(
|
||||
number = "3.2.2.2",
|
||||
text = "The Client and invocation MUST NOT have a method for supplying evaluation context.")
|
||||
@Specification(
|
||||
number = "3.2.4.1",
|
||||
text = "When the global evaluation context is set, the on context changed handler MUST run.")
|
||||
@Specification(
|
||||
number = "3.3.2.1",
|
||||
text = "The API MUST NOT have a method for setting a transaction context propagator.")
|
||||
@Test
|
||||
void not_applicable_for_dynamic_context() {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class FlagMetadataTest {
|
||||
|
||||
@Test
|
||||
|
|
@ -44,9 +44,8 @@ class FlagMetadataTest {
|
|||
@DisplayName("Value type mismatch returns a null")
|
||||
public void value_type_validation() {
|
||||
// given
|
||||
ImmutableMetadata flagMetadata = ImmutableMetadata.builder()
|
||||
.addString("string", "string")
|
||||
.build();
|
||||
ImmutableMetadata flagMetadata =
|
||||
ImmutableMetadata.builder().addString("string", "string").build();
|
||||
|
||||
// then
|
||||
assertThat(flagMetadata.getBoolean("string")).isNull();
|
||||
|
|
|
|||
|
|
@ -1,30 +1,32 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class HookContextTest {
|
||||
@Specification(number="4.2.2.2", text="Condition: The client metadata field in the hook context MUST be immutable.")
|
||||
@Specification(number="4.2.2.3", text="Condition: The provider metadata field in the hook context MUST be immutable.")
|
||||
@Test void metadata_field_is_type_metadata() {
|
||||
@Specification(
|
||||
number = "4.2.2.2",
|
||||
text = "Condition: The client metadata field in the hook context MUST be immutable.")
|
||||
@Specification(
|
||||
number = "4.2.2.3",
|
||||
text = "Condition: The provider metadata field in the hook context MUST be immutable.")
|
||||
@Test
|
||||
void metadata_field_is_type_metadata() {
|
||||
ClientMetadata clientMetadata = mock(ClientMetadata.class);
|
||||
Metadata meta = mock(Metadata.class);
|
||||
HookContext<Object> hc = HookContext.from(
|
||||
"key",
|
||||
FlagValueType.BOOLEAN,
|
||||
clientMetadata,
|
||||
meta,
|
||||
new ImmutableContext(),
|
||||
false
|
||||
);
|
||||
HookContext<Object> hc =
|
||||
HookContext.from("key", FlagValueType.BOOLEAN, clientMetadata, meta, new ImmutableContext(), false);
|
||||
|
||||
assertTrue(ClientMetadata.class.isAssignableFrom(hc.getClientMetadata().getClass()));
|
||||
assertTrue(Metadata.class.isAssignableFrom(hc.getProviderMetadata().getClass()));
|
||||
}
|
||||
|
||||
@Specification(number="4.3.3.1", text="The before stage MUST run before flag resolution occurs. It accepts a hook context (required) and hook hints (optional) as parameters. It has no return value.")
|
||||
@Test void not_applicable_for_dynamic_context() {}
|
||||
|
||||
}
|
||||
@Specification(
|
||||
number = "4.3.3.1",
|
||||
text =
|
||||
"The before stage MUST run before flag resolution occurs. It accepts a hook context (required) and hook hints (optional) as parameters. It has no return value.")
|
||||
@Test
|
||||
void not_applicable_for_dynamic_context() {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,28 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import dev.openfeature.sdk.exceptions.FlagNotFoundError;
|
||||
import dev.openfeature.sdk.fixtures.HookFixtures;
|
||||
import dev.openfeature.sdk.testutils.FeatureProviderTestUtils;
|
||||
import dev.openfeature.sdk.testutils.TestEventsProvider;
|
||||
import lombok.SneakyThrows;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
import static org.assertj.core.api.Assertions.fail;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import dev.openfeature.sdk.exceptions.FlagNotFoundError;
|
||||
import dev.openfeature.sdk.fixtures.HookFixtures;
|
||||
import dev.openfeature.sdk.testutils.FeatureProviderTestUtils;
|
||||
import dev.openfeature.sdk.testutils.TestEventsProvider;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import lombok.SneakyThrows;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
class HookSpecTest implements HookFixtures {
|
||||
@AfterEach
|
||||
void emptyApiHooks() {
|
||||
|
|
@ -25,7 +30,10 @@ class HookSpecTest implements HookFixtures {
|
|||
OpenFeatureAPI.getInstance().clearHooks();
|
||||
}
|
||||
|
||||
@Specification(number = "4.1.3", text = "The flag key, flag type, and default value properties MUST be immutable. If the language does not support immutability, the hook MUST NOT modify these properties.")
|
||||
@Specification(
|
||||
number = "4.1.3",
|
||||
text =
|
||||
"The flag key, flag type, and default value properties MUST be immutable. If the language does not support immutability, the hook MUST NOT modify these properties.")
|
||||
@Test
|
||||
void immutableValues() {
|
||||
try {
|
||||
|
|
@ -50,7 +58,10 @@ class HookSpecTest implements HookFixtures {
|
|||
}
|
||||
}
|
||||
|
||||
@Specification(number = "4.1.1", text = "Hook context MUST provide: the flag key, flag value type, evaluation context, and the default value.")
|
||||
@Specification(
|
||||
number = "4.1.1",
|
||||
text =
|
||||
"Hook context MUST provide: the flag key, flag value type, evaluation context, and the default value.")
|
||||
@Test
|
||||
void nullish_properties_on_hookcontext() {
|
||||
// missing ctx
|
||||
|
|
@ -112,10 +123,11 @@ class HookSpecTest implements HookFixtures {
|
|||
} catch (NullPointerException e) {
|
||||
fail("NPE after we provided all relevant info");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Specification(number = "4.1.2", text = "The hook context SHOULD provide: access to the client metadata and the provider metadata fields.")
|
||||
@Specification(
|
||||
number = "4.1.2",
|
||||
text = "The hook context SHOULD provide: access to the client metadata and the provider metadata fields.")
|
||||
@Test
|
||||
void optional_properties() {
|
||||
// don't specify
|
||||
|
|
@ -145,7 +157,10 @@ class HookSpecTest implements HookFixtures {
|
|||
.build();
|
||||
}
|
||||
|
||||
@Specification(number = "4.3.2.1", text = "The before stage MUST run before flag resolution occurs. It accepts a hook context (required) and hook hints (optional) as parameters and returns either an evaluation context or nothing.")
|
||||
@Specification(
|
||||
number = "4.3.2.1",
|
||||
text =
|
||||
"The before stage MUST run before flag resolution occurs. It accepts a hook context (required) and hook hints (optional) as parameters and returns either an evaluation context or nothing.")
|
||||
@Test
|
||||
void before_runs_ahead_of_evaluation() {
|
||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||
|
|
@ -153,7 +168,10 @@ class HookSpecTest implements HookFixtures {
|
|||
Client client = api.getClient();
|
||||
Hook<Boolean> evalHook = mockBooleanHook();
|
||||
|
||||
client.getBooleanValue("key", false, new ImmutableContext(),
|
||||
client.getBooleanValue(
|
||||
"key",
|
||||
false,
|
||||
new ImmutableContext(),
|
||||
FlagEvaluationOptions.builder().hook(evalHook).build());
|
||||
|
||||
verify(evalHook, times(1)).before(any(), any());
|
||||
|
|
@ -161,8 +179,7 @@ class HookSpecTest implements HookFixtures {
|
|||
|
||||
@Test
|
||||
void feo_has_hook_list() {
|
||||
FlagEvaluationOptions feo = FlagEvaluationOptions.builder()
|
||||
.build();
|
||||
FlagEvaluationOptions feo = FlagEvaluationOptions.builder().build();
|
||||
assertNotNull(feo.getHooks());
|
||||
}
|
||||
|
||||
|
|
@ -175,7 +192,6 @@ class HookSpecTest implements HookFixtures {
|
|||
verify(h, times(0)).error(any(), any(), any());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void error_hook_must_run_if_resolution_details_returns_an_error_code() {
|
||||
|
||||
|
|
@ -184,18 +200,20 @@ class HookSpecTest implements HookFixtures {
|
|||
EvaluationContext invocationCtx = new ImmutableContext();
|
||||
Hook<Boolean> hook = mockBooleanHook();
|
||||
FeatureProvider provider = mock(FeatureProvider.class);
|
||||
when(provider.getBooleanEvaluation(any(), any(), any())).thenReturn(ProviderEvaluation.<Boolean>builder()
|
||||
.errorCode(ErrorCode.FLAG_NOT_FOUND)
|
||||
.errorMessage(errorMessage)
|
||||
.build());
|
||||
when(provider.getBooleanEvaluation(any(), any(), any()))
|
||||
.thenReturn(ProviderEvaluation.<Boolean>builder()
|
||||
.errorCode(ErrorCode.FLAG_NOT_FOUND)
|
||||
.errorMessage(errorMessage)
|
||||
.build());
|
||||
|
||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||
FeatureProviderTestUtils.setFeatureProvider("errorHookMustRun", provider);
|
||||
Client client = api.getClient("errorHookMustRun");
|
||||
client.getBooleanValue("key", false, invocationCtx,
|
||||
FlagEvaluationOptions.builder()
|
||||
.hook(hook)
|
||||
.build());
|
||||
client.getBooleanValue(
|
||||
"key",
|
||||
false,
|
||||
invocationCtx,
|
||||
FlagEvaluationOptions.builder().hook(hook).build());
|
||||
|
||||
ArgumentCaptor<Exception> captor = ArgumentCaptor.forClass(Exception.class);
|
||||
|
||||
|
|
@ -209,12 +227,25 @@ class HookSpecTest implements HookFixtures {
|
|||
assertInstanceOf(FlagNotFoundError.class, exception);
|
||||
}
|
||||
|
||||
|
||||
@Specification(number = "4.3.6", text = "The after stage MUST run after flag resolution occurs. It accepts a hook context (required), flag evaluation details (required) and hook hints (optional). It has no return value.")
|
||||
@Specification(number = "4.3.7", text = "The error hook MUST run when errors are encountered in the before stage, the after stage or during flag resolution. It accepts hook context (required), exception representing what went wrong (required), and hook hints (optional). It has no return value.")
|
||||
@Specification(number = "4.3.8", text = "The finally hook MUST run after the before, after, and error stages. It accepts a hook context (required) and hook hints (optional). There is no return value.")
|
||||
@Specification(number = "4.4.1", text = "The API, Client, Provider, and invocation MUST have a method for registering hooks.")
|
||||
@Specification(number = "4.4.2", text = "Hooks MUST be evaluated in the following order: - before: API, Client, Invocation, Provider - after: Provider, Invocation, Client, API - error (if applicable): Provider, Invocation, Client, API - finally: Provider, Invocation, Client, API")
|
||||
@Specification(
|
||||
number = "4.3.6",
|
||||
text =
|
||||
"The after stage MUST run after flag resolution occurs. It accepts a hook context (required), flag evaluation details (required) and hook hints (optional). It has no return value.")
|
||||
@Specification(
|
||||
number = "4.3.7",
|
||||
text =
|
||||
"The error hook MUST run when errors are encountered in the before stage, the after stage or during flag resolution. It accepts hook context (required), exception representing what went wrong (required), and hook hints (optional). It has no return value.")
|
||||
@Specification(
|
||||
number = "4.3.8",
|
||||
text =
|
||||
"The finally hook MUST run after the before, after, and error stages. It accepts a hook context (required) and hook hints (optional). There is no return value.")
|
||||
@Specification(
|
||||
number = "4.4.1",
|
||||
text = "The API, Client, Provider, and invocation MUST have a method for registering hooks.")
|
||||
@Specification(
|
||||
number = "4.4.2",
|
||||
text =
|
||||
"Hooks MUST be evaluated in the following order: - before: API, Client, Invocation, Provider - after: Provider, Invocation, Client, API - error (if applicable): Provider, Invocation, Client, API - finally: Provider, Invocation, Client, API")
|
||||
@Test
|
||||
void hook_eval_order() {
|
||||
List<String> evalOrder = new ArrayList<>();
|
||||
|
|
@ -230,8 +261,10 @@ class HookSpecTest implements HookFixtures {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void after(HookContext<Boolean> ctx, FlagEvaluationDetails<Boolean> details, Map<String,
|
||||
Object> hints) {
|
||||
public void after(
|
||||
HookContext<Boolean> ctx,
|
||||
FlagEvaluationDetails<Boolean> details,
|
||||
Map<String, Object> hints) {
|
||||
evalOrder.add("provider after");
|
||||
}
|
||||
|
||||
|
|
@ -255,7 +288,8 @@ class HookSpecTest implements HookFixtures {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void after(HookContext<Boolean> ctx, FlagEvaluationDetails<Boolean> details, Map<String, Object> hints) {
|
||||
public void after(
|
||||
HookContext<Boolean> ctx, FlagEvaluationDetails<Boolean> details, Map<String, Object> hints) {
|
||||
evalOrder.add("api after");
|
||||
throw new RuntimeException(); // trigger error flows.
|
||||
}
|
||||
|
|
@ -280,7 +314,8 @@ class HookSpecTest implements HookFixtures {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void after(HookContext<Boolean> ctx, FlagEvaluationDetails<Boolean> details, Map<String, Object> hints) {
|
||||
public void after(
|
||||
HookContext<Boolean> ctx, FlagEvaluationDetails<Boolean> details, Map<String, Object> hints) {
|
||||
evalOrder.add("client after");
|
||||
}
|
||||
|
||||
|
|
@ -295,41 +330,63 @@ class HookSpecTest implements HookFixtures {
|
|||
}
|
||||
});
|
||||
|
||||
c.getBooleanValue("key", false, null, FlagEvaluationOptions
|
||||
.builder()
|
||||
.hook(new BooleanHook() {
|
||||
@Override
|
||||
public Optional<EvaluationContext> before(HookContext<Boolean> ctx, Map<String, Object> hints) {
|
||||
evalOrder.add("invocation before");
|
||||
return null;
|
||||
}
|
||||
c.getBooleanValue(
|
||||
"key",
|
||||
false,
|
||||
null,
|
||||
FlagEvaluationOptions.builder()
|
||||
.hook(new BooleanHook() {
|
||||
@Override
|
||||
public Optional<EvaluationContext> before(
|
||||
HookContext<Boolean> ctx, Map<String, Object> hints) {
|
||||
evalOrder.add("invocation before");
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void after(HookContext<Boolean> ctx, FlagEvaluationDetails<Boolean> details, Map<String, Object> hints) {
|
||||
evalOrder.add("invocation after");
|
||||
}
|
||||
@Override
|
||||
public void after(
|
||||
HookContext<Boolean> ctx,
|
||||
FlagEvaluationDetails<Boolean> details,
|
||||
Map<String, Object> hints) {
|
||||
evalOrder.add("invocation after");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(HookContext<Boolean> ctx, Exception error, Map<String, Object> hints) {
|
||||
evalOrder.add("invocation error");
|
||||
}
|
||||
@Override
|
||||
public void error(HookContext<Boolean> ctx, Exception error, Map<String, Object> hints) {
|
||||
evalOrder.add("invocation error");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finallyAfter(HookContext<Boolean> ctx, Map<String, Object> hints) {
|
||||
evalOrder.add("invocation finally");
|
||||
}
|
||||
})
|
||||
.build());
|
||||
@Override
|
||||
public void finallyAfter(HookContext<Boolean> ctx, Map<String, Object> hints) {
|
||||
evalOrder.add("invocation finally");
|
||||
}
|
||||
})
|
||||
.build());
|
||||
|
||||
List<String> expectedOrder = Arrays.asList(
|
||||
"api before", "client before", "invocation before", "provider before",
|
||||
"provider after", "invocation after", "client after", "api after",
|
||||
"provider error", "invocation error", "client error", "api error",
|
||||
"provider finally", "invocation finally", "client finally", "api finally");
|
||||
"api before",
|
||||
"client before",
|
||||
"invocation before",
|
||||
"provider before",
|
||||
"provider after",
|
||||
"invocation after",
|
||||
"client after",
|
||||
"api after",
|
||||
"provider error",
|
||||
"invocation error",
|
||||
"client error",
|
||||
"api error",
|
||||
"provider finally",
|
||||
"invocation finally",
|
||||
"client finally",
|
||||
"api finally");
|
||||
assertEquals(expectedOrder, evalOrder);
|
||||
}
|
||||
|
||||
@Specification(number = "4.4.6", text = "If an error occurs during the evaluation of before or after hooks, any remaining hooks in the before or after stages MUST NOT be invoked.")
|
||||
@Specification(
|
||||
number = "4.4.6",
|
||||
text =
|
||||
"If an error occurs during the evaluation of before or after hooks, any remaining hooks in the before or after stages MUST NOT be invoked.")
|
||||
@Test
|
||||
void error_stops_before() {
|
||||
Hook<Boolean> h = mockBooleanHook();
|
||||
|
|
@ -340,15 +397,19 @@ class HookSpecTest implements HookFixtures {
|
|||
api.setProviderAndWait(new AlwaysBrokenProvider());
|
||||
Client c = api.getClient();
|
||||
|
||||
c.getBooleanDetails("key", false, null, FlagEvaluationOptions.builder()
|
||||
.hook(h2)
|
||||
.hook(h)
|
||||
.build());
|
||||
verify(h, times(1)).before(any(), any());
|
||||
verify(h2, times(0)).before(any(), any());
|
||||
c.getBooleanDetails(
|
||||
"key",
|
||||
false,
|
||||
null,
|
||||
FlagEvaluationOptions.builder().hook(h2).hook(h).build());
|
||||
verify(h, times(1)).before(any(), any());
|
||||
verify(h2, times(0)).before(any(), any());
|
||||
}
|
||||
|
||||
@Specification(number = "4.4.6", text = "If an error occurs during the evaluation of before or after hooks, any remaining hooks in the before or after stages MUST NOT be invoked.")
|
||||
@Specification(
|
||||
number = "4.4.6",
|
||||
text =
|
||||
"If an error occurs during the evaluation of before or after hooks, any remaining hooks in the before or after stages MUST NOT be invoked.")
|
||||
@SneakyThrows
|
||||
@Test
|
||||
void error_stops_after() {
|
||||
|
|
@ -358,15 +419,19 @@ class HookSpecTest implements HookFixtures {
|
|||
|
||||
Client c = getClient(TestEventsProvider.newInitializedTestEventsProvider());
|
||||
|
||||
c.getBooleanDetails("key", false, null, FlagEvaluationOptions.builder()
|
||||
.hook(h)
|
||||
.hook(h2)
|
||||
.build());
|
||||
c.getBooleanDetails(
|
||||
"key",
|
||||
false,
|
||||
null,
|
||||
FlagEvaluationOptions.builder().hook(h).hook(h2).build());
|
||||
verify(h, times(1)).after(any(), any(), any());
|
||||
verify(h2, times(0)).after(any(), any(), any());
|
||||
}
|
||||
|
||||
@Specification(number = "4.2.1", text = "hook hints MUST be a structure supports definition of arbitrary properties, with keys of type string, and values of type boolean | string | number | datetime | structure..")
|
||||
@Specification(
|
||||
number = "4.2.1",
|
||||
text =
|
||||
"hook hints MUST be a structure supports definition of arbitrary properties, with keys of type string, and values of type boolean | string | number | datetime | structure..")
|
||||
@Specification(number = "4.5.2", text = "hook hints MUST be passed to each hook.")
|
||||
@Specification(number = "4.2.2.1", text = "Condition: Hook hints MUST be immutable.")
|
||||
@Specification(number = "4.5.3", text = "The hook MUST NOT alter the hook hints structure.")
|
||||
|
|
@ -378,23 +443,28 @@ class HookSpecTest implements HookFixtures {
|
|||
Hook<Boolean> mutatingHook = new BooleanHook() {
|
||||
@Override
|
||||
public Optional<EvaluationContext> before(HookContext<Boolean> ctx, Map<String, Object> hints) {
|
||||
assertThatCode(() -> hints.put(hintKey, "changed value")).isInstanceOf(UnsupportedOperationException.class);
|
||||
assertThatCode(() -> hints.put(hintKey, "changed value"))
|
||||
.isInstanceOf(UnsupportedOperationException.class);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void after(HookContext<Boolean> ctx, FlagEvaluationDetails<Boolean> details, Map<String, Object> hints) {
|
||||
assertThatCode(() -> hints.put(hintKey, "changed value")).isInstanceOf(UnsupportedOperationException.class);
|
||||
public void after(
|
||||
HookContext<Boolean> ctx, FlagEvaluationDetails<Boolean> details, Map<String, Object> hints) {
|
||||
assertThatCode(() -> hints.put(hintKey, "changed value"))
|
||||
.isInstanceOf(UnsupportedOperationException.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(HookContext<Boolean> ctx, Exception error, Map<String, Object> hints) {
|
||||
assertThatCode(() -> hints.put(hintKey, "changed value")).isInstanceOf(UnsupportedOperationException.class);
|
||||
assertThatCode(() -> hints.put(hintKey, "changed value"))
|
||||
.isInstanceOf(UnsupportedOperationException.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finallyAfter(HookContext<Boolean> ctx, Map<String, Object> hints) {
|
||||
assertThatCode(() -> hints.put(hintKey, "changed value")).isInstanceOf(UnsupportedOperationException.class);
|
||||
assertThatCode(() -> hints.put(hintKey, "changed value"))
|
||||
.isInstanceOf(UnsupportedOperationException.class);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -402,13 +472,16 @@ class HookSpecTest implements HookFixtures {
|
|||
hh.put(hintKey, "My hint value");
|
||||
hh = Collections.unmodifiableMap(hh);
|
||||
|
||||
client.getBooleanValue("key", false, new ImmutableContext(), FlagEvaluationOptions.builder()
|
||||
.hook(mutatingHook)
|
||||
.hookHints(hh)
|
||||
.build());
|
||||
client.getBooleanValue(
|
||||
"key",
|
||||
false,
|
||||
new ImmutableContext(),
|
||||
FlagEvaluationOptions.builder().hook(mutatingHook).hookHints(hh).build());
|
||||
}
|
||||
|
||||
@Specification(number = "4.5.1", text = "Flag evaluation options MAY contain hook hints, a map of data to be provided to hook invocations.")
|
||||
@Specification(
|
||||
number = "4.5.1",
|
||||
text = "Flag evaluation options MAY contain hook hints, a map of data to be provided to hook invocations.")
|
||||
@Test
|
||||
void missing_hook_hints() {
|
||||
FlagEvaluationOptions feo = FlagEvaluationOptions.builder().build();
|
||||
|
|
@ -421,15 +494,16 @@ class HookSpecTest implements HookFixtures {
|
|||
Hook hook = mockBooleanHook();
|
||||
FeatureProvider provider = mock(FeatureProvider.class);
|
||||
when(provider.getBooleanEvaluation(any(), any(), any()))
|
||||
.thenReturn(ProviderEvaluation.<Boolean>builder()
|
||||
.value(true)
|
||||
.build());
|
||||
.thenReturn(ProviderEvaluation.<Boolean>builder().value(true).build());
|
||||
InOrder order = inOrder(hook, provider);
|
||||
|
||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||
FeatureProviderTestUtils.setFeatureProvider(provider);
|
||||
Client client = api.getClient();
|
||||
client.getBooleanValue("key", false, new ImmutableContext(),
|
||||
client.getBooleanValue(
|
||||
"key",
|
||||
false,
|
||||
new ImmutableContext(),
|
||||
FlagEvaluationOptions.builder().hook(hook).build());
|
||||
|
||||
order.verify(hook).before(any(), any());
|
||||
|
|
@ -438,27 +512,39 @@ class HookSpecTest implements HookFixtures {
|
|||
order.verify(hook).finallyAfter(any(), any());
|
||||
}
|
||||
|
||||
@Specification(number = "4.4.5", text = "If an error occurs in the before or after hooks, the error hooks MUST be invoked.")
|
||||
@Specification(number = "4.4.7", text = "If an error occurs in the before hooks, the default value MUST be returned.")
|
||||
@Specification(
|
||||
number = "4.4.5",
|
||||
text = "If an error occurs in the before or after hooks, the error hooks MUST be invoked.")
|
||||
@Specification(
|
||||
number = "4.4.7",
|
||||
text = "If an error occurs in the before hooks, the default value MUST be returned.")
|
||||
@Test
|
||||
void error_hooks__before() {
|
||||
Hook hook = mockBooleanHook();
|
||||
doThrow(RuntimeException.class).when(hook).before(any(), any());
|
||||
Client client = getClient(TestEventsProvider.newInitializedTestEventsProvider());
|
||||
Boolean value = client.getBooleanValue("key", false, new ImmutableContext(),
|
||||
Boolean value = client.getBooleanValue(
|
||||
"key",
|
||||
false,
|
||||
new ImmutableContext(),
|
||||
FlagEvaluationOptions.builder().hook(hook).build());
|
||||
verify(hook, times(1)).before(any(), any());
|
||||
verify(hook, times(1)).error(any(), any(), any());
|
||||
assertEquals(false, value, "Falls through to the default.");
|
||||
}
|
||||
|
||||
@Specification(number = "4.4.5", text = "If an error occurs in the before or after hooks, the error hooks MUST be invoked.")
|
||||
@Specification(
|
||||
number = "4.4.5",
|
||||
text = "If an error occurs in the before or after hooks, the error hooks MUST be invoked.")
|
||||
@Test
|
||||
void error_hooks__after() {
|
||||
Hook hook = mockBooleanHook();
|
||||
doThrow(RuntimeException.class).when(hook).after(any(), any(), any());
|
||||
Client client = getClient(TestEventsProvider.newInitializedTestEventsProvider());
|
||||
client.getBooleanValue("key", false, new ImmutableContext(),
|
||||
client.getBooleanValue(
|
||||
"key",
|
||||
false,
|
||||
new ImmutableContext(),
|
||||
FlagEvaluationOptions.builder().hook(hook).build());
|
||||
verify(hook, times(1)).after(any(), any(), any());
|
||||
verify(hook, times(1)).error(any(), any(), any());
|
||||
|
|
@ -472,11 +558,11 @@ class HookSpecTest implements HookFixtures {
|
|||
|
||||
Client client = getClient(null);
|
||||
|
||||
client.getBooleanValue("key", false, new ImmutableContext(),
|
||||
FlagEvaluationOptions.builder()
|
||||
.hook(hook2)
|
||||
.hook(hook)
|
||||
.build());
|
||||
client.getBooleanValue(
|
||||
"key",
|
||||
false,
|
||||
new ImmutableContext(),
|
||||
FlagEvaluationOptions.builder().hook(hook2).hook(hook).build());
|
||||
|
||||
verify(hook, times(1)).before(any(), any());
|
||||
verify(hook2, times(0)).before(any(), any());
|
||||
|
|
@ -486,7 +572,10 @@ class HookSpecTest implements HookFixtures {
|
|||
}
|
||||
|
||||
@Specification(number = "4.1.4", text = "The evaluation context MUST be mutable only within the before hook.")
|
||||
@Specification(number = "4.3.4", text = "Any `evaluation context` returned from a `before` hook MUST be passed to subsequent `before` hooks (via `HookContext`).")
|
||||
@Specification(
|
||||
number = "4.3.4",
|
||||
text =
|
||||
"Any `evaluation context` returned from a `before` hook MUST be passed to subsequent `before` hooks (via `HookContext`).")
|
||||
@Test
|
||||
void beforeContextUpdated() {
|
||||
String targetingKey = "test-key";
|
||||
|
|
@ -498,11 +587,11 @@ class HookSpecTest implements HookFixtures {
|
|||
InOrder order = inOrder(hook, hook2);
|
||||
|
||||
Client client = getClient(null);
|
||||
client.getBooleanValue("key", false, ctx,
|
||||
FlagEvaluationOptions.builder()
|
||||
.hook(hook2)
|
||||
.hook(hook)
|
||||
.build());
|
||||
client.getBooleanValue(
|
||||
"key",
|
||||
false,
|
||||
ctx,
|
||||
FlagEvaluationOptions.builder().hook(hook2).hook(hook).build());
|
||||
|
||||
order.verify(hook).before(any(), any());
|
||||
ArgumentCaptor<HookContext<Boolean>> captor = ArgumentCaptor.forClass(HookContext.class);
|
||||
|
|
@ -510,10 +599,12 @@ class HookSpecTest implements HookFixtures {
|
|||
|
||||
HookContext<Boolean> hc = captor.getValue();
|
||||
assertEquals(hc.getCtx().getTargetingKey(), targetingKey);
|
||||
|
||||
}
|
||||
|
||||
@Specification(number = "4.3.5", text = "When before hooks have finished executing, any resulting evaluation context MUST be merged with the existing evaluation context.")
|
||||
@Specification(
|
||||
number = "4.3.5",
|
||||
text =
|
||||
"When before hooks have finished executing, any resulting evaluation context MUST be merged with the existing evaluation context.")
|
||||
@Test
|
||||
void mergeHappensCorrectly() {
|
||||
Map<String, Value> attributes = new HashMap<>();
|
||||
|
|
@ -521,7 +612,6 @@ class HookSpecTest implements HookFixtures {
|
|||
attributes.put("another", new Value("exists"));
|
||||
EvaluationContext hookCtx = new ImmutableContext(attributes);
|
||||
|
||||
|
||||
Map<String, Value> attributes1 = new HashMap<>();
|
||||
attributes1.put("something", new Value("here"));
|
||||
attributes1.put("test", new Value("broken"));
|
||||
|
|
@ -531,17 +621,17 @@ class HookSpecTest implements HookFixtures {
|
|||
when(hook.before(any(), any())).thenReturn(Optional.of(hookCtx));
|
||||
|
||||
FeatureProvider provider = mock(FeatureProvider.class);
|
||||
when(provider.getBooleanEvaluation(any(), any(), any())).thenReturn(ProviderEvaluation.<Boolean>builder()
|
||||
.value(true)
|
||||
.build());
|
||||
when(provider.getBooleanEvaluation(any(), any(), any()))
|
||||
.thenReturn(ProviderEvaluation.<Boolean>builder().value(true).build());
|
||||
|
||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||
FeatureProviderTestUtils.setFeatureProvider(provider);
|
||||
Client client = api.getClient();
|
||||
client.getBooleanValue("key", false, invocationCtx,
|
||||
FlagEvaluationOptions.builder()
|
||||
.hook(hook)
|
||||
.build());
|
||||
client.getBooleanValue(
|
||||
"key",
|
||||
false,
|
||||
invocationCtx,
|
||||
FlagEvaluationOptions.builder().hook(hook).build());
|
||||
|
||||
ArgumentCaptor<ImmutableContext> captor = ArgumentCaptor.forClass(ImmutableContext.class);
|
||||
verify(provider).getBooleanEvaluation(any(), any(), captor.capture());
|
||||
|
|
@ -551,7 +641,10 @@ class HookSpecTest implements HookFixtures {
|
|||
assertEquals("here", ec.getValue("something").asString());
|
||||
}
|
||||
|
||||
@Specification(number = "4.4.3", text = "If a finally hook abnormally terminates, evaluation MUST proceed, including the execution of any remaining finally hooks.")
|
||||
@Specification(
|
||||
number = "4.4.3",
|
||||
text =
|
||||
"If a finally hook abnormally terminates, evaluation MUST proceed, including the execution of any remaining finally hooks.")
|
||||
@Test
|
||||
void first_finally_broken() {
|
||||
Hook hook = mockBooleanHook();
|
||||
|
|
@ -561,18 +654,21 @@ class HookSpecTest implements HookFixtures {
|
|||
InOrder order = inOrder(hook, hook2);
|
||||
|
||||
Client client = getClient(null);
|
||||
client.getBooleanValue("key", false, new ImmutableContext(),
|
||||
FlagEvaluationOptions.builder()
|
||||
.hook(hook2)
|
||||
.hook(hook)
|
||||
.build());
|
||||
client.getBooleanValue(
|
||||
"key",
|
||||
false,
|
||||
new ImmutableContext(),
|
||||
FlagEvaluationOptions.builder().hook(hook2).hook(hook).build());
|
||||
|
||||
order.verify(hook).before(any(), any());
|
||||
order.verify(hook2).finallyAfter(any(), any());
|
||||
order.verify(hook).finallyAfter(any(), any());
|
||||
}
|
||||
|
||||
@Specification(number = "4.4.4", text = "If an error hook abnormally terminates, evaluation MUST proceed, including the execution of any remaining error hooks.")
|
||||
@Specification(
|
||||
number = "4.4.4",
|
||||
text =
|
||||
"If an error hook abnormally terminates, evaluation MUST proceed, including the execution of any remaining error hooks.")
|
||||
@Test
|
||||
void first_error_broken() {
|
||||
Hook hook = mockBooleanHook();
|
||||
|
|
@ -582,11 +678,11 @@ class HookSpecTest implements HookFixtures {
|
|||
InOrder order = inOrder(hook, hook2);
|
||||
|
||||
Client client = getClient(null);
|
||||
client.getBooleanValue("key", false, new ImmutableContext(),
|
||||
FlagEvaluationOptions.builder()
|
||||
.hook(hook2)
|
||||
.hook(hook)
|
||||
.build());
|
||||
client.getBooleanValue(
|
||||
"key",
|
||||
false,
|
||||
new ImmutableContext(),
|
||||
FlagEvaluationOptions.builder().hook(hook2).hook(hook).build());
|
||||
|
||||
order.verify(hook).before(any(), any());
|
||||
order.verify(hook2).error(any(), any(), any());
|
||||
|
|
@ -605,8 +701,7 @@ class HookSpecTest implements HookFixtures {
|
|||
|
||||
@Specification(number = "4.3.1", text = "Hooks MUST specify at least one stage.")
|
||||
@Test
|
||||
void default_methods_so_impossible() {
|
||||
}
|
||||
void default_methods_so_impossible() {}
|
||||
|
||||
@Specification(number = "4.3.9.1", text = "Instead of finally, finallyAfter SHOULD be used.")
|
||||
@SneakyThrows
|
||||
|
|
@ -619,5 +714,4 @@ class HookSpecTest implements HookFixtures {
|
|||
assertThatCode(() -> Hook.class.getMethod("finallyAfter", HookContext.class, Map.class))
|
||||
.doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,19 +5,17 @@ import static org.mockito.ArgumentMatchers.any;
|
|||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import dev.openfeature.sdk.fixtures.HookFixtures;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.EnumSource;
|
||||
|
||||
import dev.openfeature.sdk.fixtures.HookFixtures;
|
||||
|
||||
class HookSupportTest implements HookFixtures {
|
||||
@Test
|
||||
@DisplayName("should merge EvaluationContexts on before hooks correctly")
|
||||
|
|
@ -25,14 +23,16 @@ class HookSupportTest implements HookFixtures {
|
|||
Map<String, Value> attributes = new HashMap<>();
|
||||
attributes.put("baseKey", new Value("baseValue"));
|
||||
EvaluationContext baseContext = new ImmutableContext(attributes);
|
||||
HookContext<String> hookContext = new HookContext<>("flagKey", FlagValueType.STRING, "defaultValue", baseContext, () -> "client", () -> "provider");
|
||||
HookContext<String> hookContext = new HookContext<>(
|
||||
"flagKey", FlagValueType.STRING, "defaultValue", baseContext, () -> "client", () -> "provider");
|
||||
Hook<String> hook1 = mockStringHook();
|
||||
Hook<String> hook2 = mockStringHook();
|
||||
when(hook1.before(any(), any())).thenReturn(Optional.of(evaluationContextWithValue("bla", "blubber")));
|
||||
when(hook2.before(any(), any())).thenReturn(Optional.of(evaluationContextWithValue("foo", "bar")));
|
||||
HookSupport hookSupport = new HookSupport();
|
||||
|
||||
EvaluationContext result = hookSupport.beforeHooks(FlagValueType.STRING, hookContext, Arrays.asList(hook1, hook2), Collections.emptyMap());
|
||||
EvaluationContext result = hookSupport.beforeHooks(
|
||||
FlagValueType.STRING, hookContext, Arrays.asList(hook1, hook2), Collections.emptyMap());
|
||||
|
||||
assertThat(result.getValue("bla").asString()).isEqualTo("blubber");
|
||||
assertThat(result.getValue("foo").asString()).isEqualTo("bar");
|
||||
|
|
@ -47,12 +47,30 @@ class HookSupportTest implements HookFixtures {
|
|||
HookSupport hookSupport = new HookSupport();
|
||||
EvaluationContext baseContext = new ImmutableContext();
|
||||
IllegalStateException expectedException = new IllegalStateException("All fine, just a test");
|
||||
HookContext<Object> hookContext = new HookContext<>("flagKey", flagValueType, createDefaultValue(flagValueType), baseContext, () -> "client", () -> "provider");
|
||||
HookContext<Object> hookContext = new HookContext<>(
|
||||
"flagKey",
|
||||
flagValueType,
|
||||
createDefaultValue(flagValueType),
|
||||
baseContext,
|
||||
() -> "client",
|
||||
() -> "provider");
|
||||
|
||||
hookSupport.beforeHooks(flagValueType, hookContext, Collections.singletonList(genericHook), Collections.emptyMap());
|
||||
hookSupport.afterHooks(flagValueType, hookContext, FlagEvaluationDetails.builder().build(), Collections.singletonList(genericHook), Collections.emptyMap());
|
||||
hookSupport.afterAllHooks(flagValueType, hookContext, Collections.singletonList(genericHook), Collections.emptyMap());
|
||||
hookSupport.errorHooks(flagValueType, hookContext, expectedException, Collections.singletonList(genericHook), Collections.emptyMap());
|
||||
hookSupport.beforeHooks(
|
||||
flagValueType, hookContext, Collections.singletonList(genericHook), Collections.emptyMap());
|
||||
hookSupport.afterHooks(
|
||||
flagValueType,
|
||||
hookContext,
|
||||
FlagEvaluationDetails.builder().build(),
|
||||
Collections.singletonList(genericHook),
|
||||
Collections.emptyMap());
|
||||
hookSupport.afterAllHooks(
|
||||
flagValueType, hookContext, Collections.singletonList(genericHook), Collections.emptyMap());
|
||||
hookSupport.errorHooks(
|
||||
flagValueType,
|
||||
hookContext,
|
||||
expectedException,
|
||||
Collections.singletonList(genericHook),
|
||||
Collections.emptyMap());
|
||||
|
||||
verify(genericHook).before(any(), any());
|
||||
verify(genericHook).after(any(), any(), any());
|
||||
|
|
@ -83,5 +101,4 @@ class HookSupportTest implements HookFixtures {
|
|||
EvaluationContext baseContext = new ImmutableContext(attributes);
|
||||
return baseContext;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,16 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
import static dev.openfeature.sdk.EvaluationContext.TARGETING_KEY;
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ImmutableContextTest {
|
||||
@DisplayName("attributes unable to allow mutation should not affect the immutable context")
|
||||
@Test
|
||||
|
|
@ -23,7 +21,8 @@ class ImmutableContextTest {
|
|||
// should check the usage of Map.of() which is a more likely use case, but that API isn't available in Java 8
|
||||
EvaluationContext ctx = new ImmutableContext("targeting key", Collections.unmodifiableMap(attributes));
|
||||
attributes.put("key3", new Value("val3"));
|
||||
assertArrayEquals(new Object[]{"key1", "key2", TARGETING_KEY}, ctx.keySet().toArray());
|
||||
assertArrayEquals(
|
||||
new Object[] {"key1", "key2", TARGETING_KEY}, ctx.keySet().toArray());
|
||||
}
|
||||
|
||||
@DisplayName("attributes mutation should not affect the immutable context")
|
||||
|
|
@ -34,7 +33,8 @@ class ImmutableContextTest {
|
|||
attributes.put("key2", new Value("val2"));
|
||||
EvaluationContext ctx = new ImmutableContext("targeting key", attributes);
|
||||
attributes.put("key3", new Value("val3"));
|
||||
assertArrayEquals(new Object[]{"key1", "key2", TARGETING_KEY}, ctx.keySet().toArray());
|
||||
assertArrayEquals(
|
||||
new Object[] {"key1", "key2", TARGETING_KEY}, ctx.keySet().toArray());
|
||||
}
|
||||
|
||||
@DisplayName("targeting key should be changed from the overriding context")
|
||||
|
|
@ -60,6 +60,7 @@ class ImmutableContextTest {
|
|||
EvaluationContext merge = ctx.merge(overriding);
|
||||
assertEquals("targeting_key", merge.getTargetingKey());
|
||||
}
|
||||
|
||||
@DisplayName("missing targeting key should return null")
|
||||
@Test
|
||||
void missingTargetingKeyShould() {
|
||||
|
|
@ -76,10 +77,12 @@ class ImmutableContextTest {
|
|||
EvaluationContext ctx = new ImmutableContext("targeting_key", attributes);
|
||||
EvaluationContext merge = ctx.merge(null);
|
||||
assertEquals("targeting_key", merge.getTargetingKey());
|
||||
assertArrayEquals(new Object[]{"key1", "key2", TARGETING_KEY}, merge.keySet().toArray());
|
||||
assertArrayEquals(
|
||||
new Object[] {"key1", "key2", TARGETING_KEY}, merge.keySet().toArray());
|
||||
}
|
||||
|
||||
@DisplayName("Merge should retain subkeys from the existing context when the overriding context has the same targeting key")
|
||||
@DisplayName(
|
||||
"Merge should retain subkeys from the existing context when the overriding context has the same targeting key")
|
||||
@Test
|
||||
void mergeShouldRetainItsSubkeysWhenOverridingContextHasTheSameKey() {
|
||||
HashMap<String, Value> attributes = new HashMap<>();
|
||||
|
|
@ -92,21 +95,24 @@ class ImmutableContextTest {
|
|||
attributes.put("key2", new Value("val2"));
|
||||
ovKey1Attributes.put("overriding_key1_1", new Value("overriding_val_1_1"));
|
||||
overridingAttributes.put("key1", new Value(new ImmutableStructure(ovKey1Attributes)));
|
||||
|
||||
|
||||
EvaluationContext ctx = new ImmutableContext("targeting_key", attributes);
|
||||
EvaluationContext overriding = new ImmutableContext("targeting_key", overridingAttributes);
|
||||
EvaluationContext merge = ctx.merge(overriding);
|
||||
assertEquals("targeting_key", merge.getTargetingKey());
|
||||
assertArrayEquals(new Object[]{"key1", "key2", TARGETING_KEY}, merge.keySet().toArray());
|
||||
|
||||
assertArrayEquals(
|
||||
new Object[] {"key1", "key2", TARGETING_KEY}, merge.keySet().toArray());
|
||||
|
||||
Value key1 = merge.getValue("key1");
|
||||
assertTrue(key1.isStructure());
|
||||
|
||||
|
||||
Structure value = key1.asStructure();
|
||||
assertArrayEquals(new Object[]{"key1_1","overriding_key1_1"}, value.keySet().toArray());
|
||||
assertArrayEquals(
|
||||
new Object[] {"key1_1", "overriding_key1_1"}, value.keySet().toArray());
|
||||
}
|
||||
|
||||
@DisplayName("Merge should retain subkeys from the existing context when the overriding context doesn't have targeting key")
|
||||
|
||||
@DisplayName(
|
||||
"Merge should retain subkeys from the existing context when the overriding context doesn't have targeting key")
|
||||
@Test
|
||||
void mergeShouldRetainItsSubkeysWhenOverridingContextHasNoTargetingKey() {
|
||||
HashMap<String, Value> attributes = new HashMap<>();
|
||||
|
|
@ -115,16 +121,16 @@ class ImmutableContextTest {
|
|||
key1Attributes.put("key1_1", new Value("val1_1"));
|
||||
attributes.put("key1", new Value(new ImmutableStructure(key1Attributes)));
|
||||
attributes.put("key2", new Value("val2"));
|
||||
|
||||
|
||||
EvaluationContext ctx = new ImmutableContext(attributes);
|
||||
EvaluationContext overriding = new ImmutableContext();
|
||||
EvaluationContext merge = ctx.merge(overriding);
|
||||
assertArrayEquals(new Object[]{"key1", "key2"}, merge.keySet().toArray());
|
||||
|
||||
assertArrayEquals(new Object[] {"key1", "key2"}, merge.keySet().toArray());
|
||||
|
||||
Value key1 = merge.getValue("key1");
|
||||
assertTrue(key1.isStructure());
|
||||
|
||||
|
||||
Structure value = key1.asStructure();
|
||||
assertArrayEquals(new Object[]{"key1_1"}, value.keySet().toArray());
|
||||
assertArrayEquals(new Object[] {"key1_1"}, value.keySet().toArray());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
|
@ -9,16 +9,17 @@ import java.util.HashMap;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ImmutableStructureTest {
|
||||
@Test void noArgShouldContainEmptyAttributes() {
|
||||
@Test
|
||||
void noArgShouldContainEmptyAttributes() {
|
||||
ImmutableStructure structure = new ImmutableStructure();
|
||||
assertEquals(0, structure.asMap().keySet().size());
|
||||
}
|
||||
|
||||
@Test void mapArgShouldContainNewMap() {
|
||||
@Test
|
||||
void mapArgShouldContainNewMap() {
|
||||
String KEY = "key";
|
||||
Map<String, Value> map = new HashMap<String, Value>() {
|
||||
{
|
||||
|
|
@ -30,7 +31,8 @@ class ImmutableStructureTest {
|
|||
assertNotSame(structure.asMap(), map); // should be a copy
|
||||
}
|
||||
|
||||
@Test void MutatingGetValueShouldNotChangeOriginalValue() {
|
||||
@Test
|
||||
void MutatingGetValueShouldNotChangeOriginalValue() {
|
||||
String KEY = "key";
|
||||
List<Value> lists = new ArrayList<>();
|
||||
lists.add(new Value(KEY));
|
||||
|
|
@ -47,7 +49,8 @@ class ImmutableStructureTest {
|
|||
assertNotSame(structure.asMap(), map); // should be a copy
|
||||
}
|
||||
|
||||
@Test void MutatingGetInstantValueShouldNotChangeOriginalValue() {
|
||||
@Test
|
||||
void MutatingGetInstantValueShouldNotChangeOriginalValue() {
|
||||
String KEY = "key";
|
||||
Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS);
|
||||
Map<String, Value> map = new HashMap<String, Value>() {
|
||||
|
|
@ -56,39 +59,60 @@ class ImmutableStructureTest {
|
|||
}
|
||||
};
|
||||
ImmutableStructure structure = new ImmutableStructure(map);
|
||||
//mutate the original value
|
||||
// mutate the original value
|
||||
Instant tomorrow = now.plus(1, ChronoUnit.DAYS);
|
||||
//mutate the getValue
|
||||
// mutate the getValue
|
||||
structure.getValue(KEY).asInstant().plus(1, ChronoUnit.DAYS);
|
||||
|
||||
assertNotEquals(tomorrow, structure.getValue(KEY).asInstant());
|
||||
assertEquals(now, structure.getValue(KEY).asInstant());
|
||||
}
|
||||
|
||||
@Test void MutatingGetStructureValueShouldNotChangeOriginalValue() {
|
||||
@Test
|
||||
void MutatingGetStructureValueShouldNotChangeOriginalValue() {
|
||||
String KEY = "key";
|
||||
List<Value> lists = new ArrayList<>();
|
||||
lists.add(new Value("dummy_list_1"));
|
||||
MutableStructure mutableStructure = new MutableStructure().add("key1","val1").add("list", lists);
|
||||
MutableStructure mutableStructure =
|
||||
new MutableStructure().add("key1", "val1").add("list", lists);
|
||||
Map<String, Value> map = new HashMap<String, Value>() {
|
||||
{
|
||||
put(KEY, new Value(mutableStructure));
|
||||
}
|
||||
};
|
||||
ImmutableStructure structure = new ImmutableStructure(map);
|
||||
//mutate the original structure
|
||||
// mutate the original structure
|
||||
mutableStructure.add("key2", "val2");
|
||||
//mutate the return value
|
||||
// mutate the return value
|
||||
structure.getValue(KEY).asStructure().asMap().put("key3", new Value("val3"));
|
||||
assertEquals(2, structure.getValue(KEY).asStructure().asMap().size());
|
||||
assertArrayEquals(new Object[]{"key1", "list"}, structure.getValue(KEY).asStructure().keySet().toArray());
|
||||
assertArrayEquals(
|
||||
new Object[] {"key1", "list"},
|
||||
structure.getValue(KEY).asStructure().keySet().toArray());
|
||||
assertTrue(structure.getValue(KEY).asStructure() instanceof ImmutableStructure);
|
||||
//mutate list value
|
||||
// mutate list value
|
||||
lists.add(new Value("dummy_list_2"));
|
||||
//mutate the return list value
|
||||
// mutate the return list value
|
||||
structure.getValue(KEY).asStructure().asMap().get("list").asList().add(new Value("dummy_list_3"));
|
||||
assertEquals(1, structure.getValue(KEY).asStructure().asMap().get("list").asList().size());
|
||||
assertEquals("dummy_list_1", structure.getValue(KEY).asStructure().asMap().get("list").asList().get(0).asString());
|
||||
assertEquals(
|
||||
1,
|
||||
structure
|
||||
.getValue(KEY)
|
||||
.asStructure()
|
||||
.asMap()
|
||||
.get("list")
|
||||
.asList()
|
||||
.size());
|
||||
assertEquals(
|
||||
"dummy_list_1",
|
||||
structure
|
||||
.getValue(KEY)
|
||||
.asStructure()
|
||||
.asMap()
|
||||
.get("list")
|
||||
.asList()
|
||||
.get(0)
|
||||
.asString());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -112,7 +136,8 @@ class ImmutableStructureTest {
|
|||
assertNull(value);
|
||||
}
|
||||
|
||||
@Test void objectMapTest() {
|
||||
@Test
|
||||
void objectMapTest() {
|
||||
Map<String, Value> attrs = new HashMap<>();
|
||||
attrs.put("test", new Value(45));
|
||||
ImmutableStructure structure = new ImmutableStructure(attrs);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,18 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import dev.openfeature.sdk.testutils.exception.TestException;
|
||||
import org.junit.jupiter.api.*;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.mockito.Mockito.any;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.timeout;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import dev.openfeature.sdk.testutils.exception.TestException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class InitializeBehaviorSpecTest {
|
||||
|
||||
|
|
@ -18,11 +26,13 @@ class InitializeBehaviorSpecTest {
|
|||
@Nested
|
||||
class DefaultProvider {
|
||||
|
||||
@Specification(number = "1.1.2.2", text = "The `provider mutator` function MUST invoke the `initialize` "
|
||||
+ "function on the newly registered provider before using it to resolve flag values.")
|
||||
@Specification(
|
||||
number = "1.1.2.2",
|
||||
text = "The `provider mutator` function MUST invoke the `initialize` "
|
||||
+ "function on the newly registered provider before using it to resolve flag values.")
|
||||
@Test
|
||||
@DisplayName("must call initialize function of the newly registered provider before using it for "
|
||||
+ "flag evaluation")
|
||||
+ "flag evaluation")
|
||||
void mustCallInitializeFunctionOfTheNewlyRegisteredProviderBeforeUsingItForFlagEvaluation() throws Exception {
|
||||
FeatureProvider featureProvider = mock(FeatureProvider.class);
|
||||
doReturn(ProviderState.NOT_READY).when(featureProvider).getState();
|
||||
|
|
@ -32,10 +42,12 @@ class InitializeBehaviorSpecTest {
|
|||
verify(featureProvider, timeout(1000)).initialize(any());
|
||||
}
|
||||
|
||||
@Specification(number = "1.4.10", text = "Methods, functions, or operations on the client MUST NOT throw "
|
||||
+ "exceptions, or otherwise abnormally terminate. Flag evaluation calls must always return the "
|
||||
+ "`default value` in the event of abnormal execution. Exceptions include functions or methods for "
|
||||
+ "the purposes for configuration or setup.")
|
||||
@Specification(
|
||||
number = "1.4.10",
|
||||
text = "Methods, functions, or operations on the client MUST NOT throw "
|
||||
+ "exceptions, or otherwise abnormally terminate. Flag evaluation calls must always return the "
|
||||
+ "`default value` in the event of abnormal execution. Exceptions include functions or methods for "
|
||||
+ "the purposes for configuration or setup.")
|
||||
@Test
|
||||
@DisplayName("should catch exception thrown by the provider on initialization")
|
||||
void shouldCatchExceptionThrownByTheProviderOnInitialization() throws Exception {
|
||||
|
|
@ -44,7 +56,7 @@ class InitializeBehaviorSpecTest {
|
|||
doThrow(TestException.class).when(featureProvider).initialize(any());
|
||||
|
||||
assertThatCode(() -> OpenFeatureAPI.getInstance().setProvider(featureProvider))
|
||||
.doesNotThrowAnyException();
|
||||
.doesNotThrowAnyException();
|
||||
|
||||
verify(featureProvider, timeout(1000)).initialize(any());
|
||||
}
|
||||
|
|
@ -53,12 +65,15 @@ class InitializeBehaviorSpecTest {
|
|||
@Nested
|
||||
class ProviderForNamedClient {
|
||||
|
||||
@Specification(number = "1.1.2.2", text = "The `provider mutator` function MUST invoke the `initialize`"
|
||||
+ " function on the newly registered provider before using it to resolve flag values.")
|
||||
@Specification(
|
||||
number = "1.1.2.2",
|
||||
text = "The `provider mutator` function MUST invoke the `initialize`"
|
||||
+ " function on the newly registered provider before using it to resolve flag values.")
|
||||
@Test
|
||||
@DisplayName("must call initialize function of the newly registered named provider before using it "
|
||||
+ "for flag evaluation")
|
||||
void mustCallInitializeFunctionOfTheNewlyRegisteredNamedProviderBeforeUsingItForFlagEvaluation() throws Exception {
|
||||
+ "for flag evaluation")
|
||||
void mustCallInitializeFunctionOfTheNewlyRegisteredNamedProviderBeforeUsingItForFlagEvaluation()
|
||||
throws Exception {
|
||||
FeatureProvider featureProvider = mock(FeatureProvider.class);
|
||||
doReturn(ProviderState.NOT_READY).when(featureProvider).getState();
|
||||
|
||||
|
|
@ -67,10 +82,12 @@ class InitializeBehaviorSpecTest {
|
|||
verify(featureProvider, timeout(1000)).initialize(any());
|
||||
}
|
||||
|
||||
@Specification(number = "1.4.10", text = "Methods, functions, or operations on the client MUST NOT throw "
|
||||
+ "exceptions, or otherwise abnormally terminate. Flag evaluation calls must always return the "
|
||||
+ "`default value` in the event of abnormal execution. Exceptions include functions or methods for "
|
||||
+ "the purposes for configuration or setup.")
|
||||
@Specification(
|
||||
number = "1.4.10",
|
||||
text = "Methods, functions, or operations on the client MUST NOT throw "
|
||||
+ "exceptions, or otherwise abnormally terminate. Flag evaluation calls must always return the "
|
||||
+ "`default value` in the event of abnormal execution. Exceptions include functions or methods for "
|
||||
+ "the purposes for configuration or setup.")
|
||||
@Test
|
||||
@DisplayName("should catch exception thrown by the named client provider on initialization")
|
||||
void shouldCatchExceptionThrownByTheNamedClientProviderOnInitialization() throws Exception {
|
||||
|
|
@ -79,7 +96,7 @@ class InitializeBehaviorSpecTest {
|
|||
doThrow(TestException.class).when(featureProvider).initialize(any());
|
||||
|
||||
assertThatCode(() -> OpenFeatureAPI.getInstance().setProvider(DOMAIN_NAME, featureProvider))
|
||||
.doesNotThrowAnyException();
|
||||
.doesNotThrowAnyException();
|
||||
|
||||
verify(featureProvider, timeout(1000)).initialize(any());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,26 +5,24 @@ import static org.mockito.Mockito.mock;
|
|||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.parallel.Isolated;
|
||||
|
||||
import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock;
|
||||
|
||||
@Isolated()
|
||||
class LockingTest {
|
||||
|
||||
|
||||
private static OpenFeatureAPI api;
|
||||
private OpenFeatureClient client;
|
||||
private AutoCloseableReentrantReadWriteLock apiLock;
|
||||
private AutoCloseableReentrantReadWriteLock clientContextLock;
|
||||
private AutoCloseableReentrantReadWriteLock clientHooksLock;
|
||||
|
||||
|
||||
@BeforeAll
|
||||
static void beforeAll() {
|
||||
api = OpenFeatureAPI.getInstance();
|
||||
|
|
@ -34,7 +32,7 @@ class LockingTest {
|
|||
@BeforeEach
|
||||
void beforeEach() {
|
||||
client = (OpenFeatureClient) api.getClient("LockingTest");
|
||||
|
||||
|
||||
apiLock = setupLock(apiLock, mockInnerReadLock(), mockInnerWriteLock());
|
||||
OpenFeatureAPI.lock = apiLock;
|
||||
|
||||
|
|
@ -93,8 +91,9 @@ class LockingTest {
|
|||
|
||||
@Nested
|
||||
class Client {
|
||||
|
||||
// Note that the API lock is used for adding client handlers, they are all added (indirectly) on the API object.
|
||||
|
||||
// Note that the API lock is used for adding client handlers, they are all added (indirectly) on the API
|
||||
// object.
|
||||
|
||||
@Test
|
||||
void onShouldApiWriteLockAndUnlock() {
|
||||
|
|
@ -138,16 +137,13 @@ class LockingTest {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void addHooksShouldWriteLockAndUnlock() {
|
||||
client.addHooks(new Hook() {
|
||||
});
|
||||
client.addHooks(new Hook() {});
|
||||
verify(clientHooksLock.writeLock()).lock();
|
||||
verify(clientHooksLock.writeLock()).unlock();
|
||||
|
||||
api.addHooks(new Hook() {
|
||||
});
|
||||
api.addHooks(new Hook() {});
|
||||
verify(apiLock.writeLock()).lock();
|
||||
verify(apiLock.writeLock()).unlock();
|
||||
}
|
||||
|
|
@ -199,7 +195,6 @@ class LockingTest {
|
|||
verify(apiLock.readLock()).unlock();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void clearHooksShouldWriteLockAndUnlock() {
|
||||
api.clearHooks();
|
||||
|
|
@ -221,7 +216,8 @@ class LockingTest {
|
|||
return writeLockMock;
|
||||
}
|
||||
|
||||
private AutoCloseableReentrantReadWriteLock setupLock(AutoCloseableReentrantReadWriteLock lock,
|
||||
private AutoCloseableReentrantReadWriteLock setupLock(
|
||||
AutoCloseableReentrantReadWriteLock lock,
|
||||
AutoCloseableReentrantReadWriteLock.ReadLock readlock,
|
||||
AutoCloseableReentrantReadWriteLock.WriteLock writeLock) {
|
||||
lock = mock(AutoCloseableReentrantReadWriteLock.class);
|
||||
|
|
@ -231,4 +227,4 @@ class LockingTest {
|
|||
when(lock.writeLock()).thenReturn(writeLock);
|
||||
return lock;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class MetadataTest {
|
||||
@Specification(number="4.2.2.2", text="Condition: The client metadata field in the hook context MUST be immutable.")
|
||||
@Specification(number="4.2.2.3", text="Condition: The provider metadata field in the hook context MUST be immutable.")
|
||||
@Specification(
|
||||
number = "4.2.2.2",
|
||||
text = "Condition: The client metadata field in the hook context MUST be immutable.")
|
||||
@Specification(
|
||||
number = "4.2.2.3",
|
||||
text = "Condition: The provider metadata field in the hook context MUST be immutable.")
|
||||
@Test
|
||||
void metadata_is_immutable() {
|
||||
try {
|
||||
|
|
@ -16,4 +20,4 @@ class MetadataTest {
|
|||
// Pass
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,16 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static dev.openfeature.sdk.EvaluationContext.TARGETING_KEY;
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class MutableContextTest {
|
||||
|
||||
@DisplayName("attributes unable to allow mutation should not affect the Mutable context")
|
||||
|
|
@ -23,7 +22,8 @@ class MutableContextTest {
|
|||
// should check the usage of Map.of() which is a more likely use case, but that API isn't available in Java 8
|
||||
EvaluationContext ctx = new MutableContext("targeting key", Collections.unmodifiableMap(attributes));
|
||||
attributes.put("key3", new Value("val3"));
|
||||
assertArrayEquals(new Object[]{"key1", "key2", TARGETING_KEY}, ctx.keySet().toArray());
|
||||
assertArrayEquals(
|
||||
new Object[] {"key1", "key2", TARGETING_KEY}, ctx.keySet().toArray());
|
||||
}
|
||||
|
||||
@DisplayName("targeting key should be changed from the overriding context")
|
||||
|
|
@ -49,6 +49,7 @@ class MutableContextTest {
|
|||
EvaluationContext merge = ctx.merge(overriding);
|
||||
assertEquals("targeting_key", merge.getTargetingKey());
|
||||
}
|
||||
|
||||
@DisplayName("missing targeting key should return null")
|
||||
@Test
|
||||
void missingTargetingKeyShould() {
|
||||
|
|
@ -65,10 +66,12 @@ class MutableContextTest {
|
|||
EvaluationContext ctx = new MutableContext("targeting_key", attributes);
|
||||
EvaluationContext merge = ctx.merge(null);
|
||||
assertEquals("targeting_key", merge.getTargetingKey());
|
||||
assertArrayEquals(new Object[]{"key1", "key2", TARGETING_KEY}, merge.keySet().toArray());
|
||||
assertArrayEquals(
|
||||
new Object[] {"key1", "key2", TARGETING_KEY}, merge.keySet().toArray());
|
||||
}
|
||||
|
||||
@DisplayName("Merge should retain subkeys from the existing context when the overriding context has the same targeting key")
|
||||
@DisplayName(
|
||||
"Merge should retain subkeys from the existing context when the overriding context has the same targeting key")
|
||||
@Test
|
||||
void mergeShouldRetainItsSubkeysWhenOverridingContextHasTheSameKey() {
|
||||
HashMap<String, Value> attributes = new HashMap<>();
|
||||
|
|
@ -81,21 +84,24 @@ class MutableContextTest {
|
|||
attributes.put("key2", new Value("val2"));
|
||||
ovKey1Attributes.put("overriding_key1_1", new Value("overriding_val_1_1"));
|
||||
overridingAttributes.put("key1", new Value(new ImmutableStructure(ovKey1Attributes)));
|
||||
|
||||
|
||||
EvaluationContext ctx = new MutableContext("targeting_key", attributes);
|
||||
EvaluationContext overriding = new MutableContext("targeting_key", overridingAttributes);
|
||||
EvaluationContext merge = ctx.merge(overriding);
|
||||
assertEquals("targeting_key", merge.getTargetingKey());
|
||||
assertArrayEquals(new Object[]{"key1", "key2", TARGETING_KEY}, merge.keySet().toArray());
|
||||
|
||||
assertArrayEquals(
|
||||
new Object[] {"key1", "key2", TARGETING_KEY}, merge.keySet().toArray());
|
||||
|
||||
Value key1 = merge.getValue("key1");
|
||||
assertTrue(key1.isStructure());
|
||||
|
||||
|
||||
Structure value = key1.asStructure();
|
||||
assertArrayEquals(new Object[]{"key1_1","overriding_key1_1"}, value.keySet().toArray());
|
||||
assertArrayEquals(
|
||||
new Object[] {"key1_1", "overriding_key1_1"}, value.keySet().toArray());
|
||||
}
|
||||
|
||||
@DisplayName("Merge should retain subkeys from the existing context when the overriding context doesn't have targeting key")
|
||||
|
||||
@DisplayName(
|
||||
"Merge should retain subkeys from the existing context when the overriding context doesn't have targeting key")
|
||||
@Test
|
||||
void mergeShouldRetainItsSubkeysWhenOverridingContextHasNoTargetingKey() {
|
||||
HashMap<String, Value> attributes = new HashMap<>();
|
||||
|
|
@ -104,17 +110,17 @@ class MutableContextTest {
|
|||
key1Attributes.put("key1_1", new Value("val1_1"));
|
||||
attributes.put("key1", new Value(new ImmutableStructure(key1Attributes)));
|
||||
attributes.put("key2", new Value("val2"));
|
||||
|
||||
|
||||
EvaluationContext ctx = new MutableContext(attributes);
|
||||
EvaluationContext overriding = new MutableContext();
|
||||
EvaluationContext merge = ctx.merge(overriding);
|
||||
assertArrayEquals(new Object[]{"key1", "key2"}, merge.keySet().toArray());
|
||||
|
||||
assertArrayEquals(new Object[] {"key1", "key2"}, merge.keySet().toArray());
|
||||
|
||||
Value key1 = merge.getValue("key1");
|
||||
assertTrue(key1.isStructure());
|
||||
|
||||
|
||||
Structure value = key1.asStructure();
|
||||
assertArrayEquals(new Object[]{"key1_1"}, value.keySet().toArray());
|
||||
assertArrayEquals(new Object[] {"key1_1"}, value.keySet().toArray());
|
||||
}
|
||||
|
||||
@DisplayName("Ensure mutations are chainable")
|
||||
|
|
@ -129,6 +135,6 @@ class MutableContextTest {
|
|||
assertEquals("TARGETING_KEY", context.getTargetingKey());
|
||||
assertEquals("val1", context.getValue("key1").asString());
|
||||
assertEquals(2, context.getValue("key2").asInteger());
|
||||
assertEquals(3.0, context.getValue("key3").asDouble());
|
||||
assertEquals(3.0, context.getValue("key3").asDouble());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,15 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
|
||||
class MutableTrackingEventDetailsTest {
|
||||
import com.google.common.collect.Lists;
|
||||
import java.time.Instant;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class MutableTrackingEventDetailsTest {
|
||||
|
||||
@Test
|
||||
void hasDefaultValue() {
|
||||
|
|
@ -46,6 +44,8 @@ class MutableTrackingEventDetailsTest {
|
|||
assertEquals(new Value(Instant.parse("2023-12-03T10:15:30Z")), track.getValue("key5"));
|
||||
assertEquals(new Value(new MutableContext()), track.getValue("key6"));
|
||||
assertEquals(new Value(7), track.getValue("key7"));
|
||||
assertArrayEquals(new Object[]{new Value(8), new Value(9)}, track.getValue("key8").asList().toArray());
|
||||
assertArrayEquals(
|
||||
new Object[] {new Value(8), new Value(9)},
|
||||
track.getValue("key8").asList().toArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,32 +5,37 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
|||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class NoOpProviderTest {
|
||||
@Test void bool() {
|
||||
@Test
|
||||
void bool() {
|
||||
NoOpProvider p = new NoOpProvider();
|
||||
ProviderEvaluation<Boolean> eval = p.getBooleanEvaluation("key", true, null);
|
||||
assertEquals(true, eval.getValue());
|
||||
}
|
||||
|
||||
@Test void str() {
|
||||
@Test
|
||||
void str() {
|
||||
NoOpProvider p = new NoOpProvider();
|
||||
|
||||
ProviderEvaluation<String> eval = p.getStringEvaluation("key", "works", null);
|
||||
assertEquals("works", eval.getValue());
|
||||
}
|
||||
|
||||
@Test void integer() {
|
||||
@Test
|
||||
void integer() {
|
||||
NoOpProvider p = new NoOpProvider();
|
||||
ProviderEvaluation<Integer> eval = p.getIntegerEvaluation("key", 4, null);
|
||||
assertEquals(4, eval.getValue());
|
||||
}
|
||||
|
||||
@Test void noOpdouble() {
|
||||
@Test
|
||||
void noOpdouble() {
|
||||
NoOpProvider p = new NoOpProvider();
|
||||
ProviderEvaluation<Double> eval = p.getDoubleEvaluation("key", 0.4, null);
|
||||
assertEquals(0.4, eval.getValue());
|
||||
}
|
||||
|
||||
@Test void value() {
|
||||
@Test
|
||||
void value() {
|
||||
NoOpProvider p = new NoOpProvider();
|
||||
Value s = new Value();
|
||||
ProviderEvaluation<Value> eval = p.getObjectEvaluation("key", s, null);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class NoOpTransactionContextPropagatorTest {
|
||||
|
||||
|
|
@ -26,4 +25,4 @@ class NoOpTransactionContextPropagatorTest {
|
|||
EvaluationContext result = contextPropagator.getTransactionContext();
|
||||
assertTrue(result.asMap().isEmpty());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ public class NotImplementedException extends RuntimeException {
|
|||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public NotImplementedException(String message){
|
||||
public NotImplementedException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,5 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import dev.openfeature.sdk.providers.memory.InMemoryProvider;
|
||||
import dev.openfeature.sdk.testutils.FeatureProviderTestUtils;
|
||||
import dev.openfeature.sdk.testutils.TestEventsProvider;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
|
@ -16,6 +7,14 @@ import static org.mockito.ArgumentMatchers.any;
|
|||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import dev.openfeature.sdk.providers.memory.InMemoryProvider;
|
||||
import dev.openfeature.sdk.testutils.FeatureProviderTestUtils;
|
||||
import dev.openfeature.sdk.testutils.TestEventsProvider;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class OpenFeatureAPITest {
|
||||
|
||||
private static final String DOMAIN_NAME = "my domain";
|
||||
|
|
@ -36,7 +35,10 @@ class OpenFeatureAPITest {
|
|||
.isEqualTo(api.getProviderMetadata("namedProviderTest").getName());
|
||||
}
|
||||
|
||||
@Specification(number = "1.1.3", text = "The API MUST provide a function to bind a given provider to one or more clients using a domain. If the domain already has a bound provider, it is overwritten with the new mapping.")
|
||||
@Specification(
|
||||
number = "1.1.3",
|
||||
text =
|
||||
"The API MUST provide a function to bind a given provider to one or more clients using a domain. If the domain already has a bound provider, it is overwritten with the new mapping.")
|
||||
@Test
|
||||
void namedProviderOverwrittenTest() {
|
||||
String domain = "namedProviderOverwrittenTest";
|
||||
|
|
@ -45,7 +47,10 @@ class OpenFeatureAPITest {
|
|||
FeatureProviderTestUtils.setFeatureProvider(domain, provider1);
|
||||
FeatureProviderTestUtils.setFeatureProvider(domain, provider2);
|
||||
|
||||
assertThat(OpenFeatureAPI.getInstance().getProvider(domain).getMetadata().getName())
|
||||
assertThat(OpenFeatureAPI.getInstance()
|
||||
.getProvider(domain)
|
||||
.getMetadata()
|
||||
.getName())
|
||||
.isEqualTo(DoSomethingProvider.name);
|
||||
}
|
||||
|
||||
|
|
@ -105,7 +110,6 @@ class OpenFeatureAPITest {
|
|||
.isEqualTo(ProviderState.READY);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void featureProviderTrackIsCalled() throws Exception {
|
||||
FeatureProvider featureProvider = mock(FeatureProvider.class);
|
||||
|
|
@ -113,9 +117,7 @@ class OpenFeatureAPITest {
|
|||
|
||||
OpenFeatureAPI.getInstance()
|
||||
.getClient()
|
||||
.track("track-event",
|
||||
new ImmutableContext(),
|
||||
new MutableTrackingEventDetails(22.2f));
|
||||
.track("track-event", new ImmutableContext(), new MutableTrackingEventDetails(22.2f));
|
||||
|
||||
verify(featureProvider).initialize(any());
|
||||
verify(featureProvider).getMetadata();
|
||||
|
|
|
|||
|
|
@ -7,9 +7,11 @@ import static org.mockito.ArgumentMatchers.any;
|
|||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import dev.openfeature.sdk.exceptions.FatalError;
|
||||
import dev.openfeature.sdk.fixtures.HookFixtures;
|
||||
import dev.openfeature.sdk.testutils.TestEventsProvider;
|
||||
import java.util.HashMap;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
|
|
@ -18,10 +20,6 @@ import org.mockito.Mockito;
|
|||
import org.simplify4u.slf4jmock.LoggerMock;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import dev.openfeature.sdk.exceptions.FatalError;
|
||||
import dev.openfeature.sdk.fixtures.HookFixtures;
|
||||
import dev.openfeature.sdk.testutils.TestEventsProvider;
|
||||
|
||||
class OpenFeatureClientTest implements HookFixtures {
|
||||
|
||||
private Logger logger;
|
||||
|
|
@ -41,13 +39,15 @@ class OpenFeatureClientTest implements HookFixtures {
|
|||
@DisplayName("should not throw exception if hook has different type argument than hookContext")
|
||||
void shouldNotThrowExceptionIfHookHasDifferentTypeArgumentThanHookContext() {
|
||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||
api.setProviderAndWait("shouldNotThrowExceptionIfHookHasDifferentTypeArgumentThanHookContext", new DoSomethingProvider());
|
||||
api.setProviderAndWait(
|
||||
"shouldNotThrowExceptionIfHookHasDifferentTypeArgumentThanHookContext", new DoSomethingProvider());
|
||||
Client client = api.getClient("shouldNotThrowExceptionIfHookHasDifferentTypeArgumentThanHookContext");
|
||||
client.addHooks(mockBooleanHook(), mockStringHook());
|
||||
FlagEvaluationDetails<Boolean> actual = client.getBooleanDetails("feature key", Boolean.FALSE);
|
||||
|
||||
assertThat(actual.getValue()).isTrue();
|
||||
// I dislike this, but given the mocking tools available, there's no way that I know of to say "no errors were logged"
|
||||
// I dislike this, but given the mocking tools available, there's no way that I know of to say "no errors were
|
||||
// logged"
|
||||
Mockito.verify(logger, never()).error(any());
|
||||
Mockito.verify(logger, never()).error(anyString(), any(Throwable.class));
|
||||
Mockito.verify(logger, never()).error(anyString(), any(Object.class));
|
||||
|
|
@ -78,7 +78,6 @@ class OpenFeatureClientTest implements HookFixtures {
|
|||
assertEquals(client, result);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
@DisplayName("Should not call evaluation methods when the provider has state FATAL")
|
||||
void shouldNotCallEvaluationMethodsWhenProviderIsInFatalErrorState() {
|
||||
|
|
@ -86,7 +85,10 @@ class OpenFeatureClientTest implements HookFixtures {
|
|||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||
Client client = api.getClient("shouldNotCallEvaluationMethodsWhenProviderIsInFatalErrorState");
|
||||
|
||||
assertThrows(FatalError.class, () -> api.setProviderAndWait("shouldNotCallEvaluationMethodsWhenProviderIsInFatalErrorState", provider));
|
||||
assertThrows(
|
||||
FatalError.class,
|
||||
() -> api.setProviderAndWait(
|
||||
"shouldNotCallEvaluationMethodsWhenProviderIsInFatalErrorState", provider));
|
||||
FlagEvaluationDetails<Boolean> details = client.getBooleanDetails("key", true);
|
||||
assertThat(details.getErrorCode()).isEqualTo(ErrorCode.PROVIDER_FATAL);
|
||||
}
|
||||
|
|
@ -126,7 +128,8 @@ class OpenFeatureClientTest implements HookFixtures {
|
|||
}
|
||||
|
||||
@Override
|
||||
public ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) {
|
||||
public ProviderEvaluation<Boolean> getBooleanEvaluation(
|
||||
String key, Boolean defaultValue, EvaluationContext ctx) {
|
||||
evaluationCalled.set(true);
|
||||
return null;
|
||||
}
|
||||
|
|
@ -138,7 +141,8 @@ class OpenFeatureClientTest implements HookFixtures {
|
|||
}
|
||||
|
||||
@Override
|
||||
public ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) {
|
||||
public ProviderEvaluation<Integer> getIntegerEvaluation(
|
||||
String key, Integer defaultValue, EvaluationContext ctx) {
|
||||
evaluationCalled.set(true);
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,13 +27,8 @@ class ProviderEvaluationTest {
|
|||
String errorMessage = "message";
|
||||
ImmutableMetadata metadata = ImmutableMetadata.builder().build();
|
||||
|
||||
ProviderEvaluation<Integer> details = new ProviderEvaluation<>(
|
||||
value,
|
||||
variant,
|
||||
reason.toString(),
|
||||
errorCode,
|
||||
errorMessage,
|
||||
metadata);
|
||||
ProviderEvaluation<Integer> details =
|
||||
new ProviderEvaluation<>(value, variant, reason.toString(), errorCode, errorMessage, metadata);
|
||||
|
||||
assertEquals(value, details.getValue());
|
||||
assertEquals(variant, details.getVariant());
|
||||
|
|
|
|||
|
|
@ -1,20 +1,5 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import dev.openfeature.sdk.exceptions.OpenFeatureError;
|
||||
import dev.openfeature.sdk.testutils.exception.TestException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static dev.openfeature.sdk.fixtures.ProviderFixture.*;
|
||||
import static dev.openfeature.sdk.testutils.stubbing.ConditionStubber.doDelayResponse;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
|
@ -24,6 +9,20 @@ import static org.mockito.ArgumentMatchers.any;
|
|||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import dev.openfeature.sdk.exceptions.OpenFeatureError;
|
||||
import dev.openfeature.sdk.testutils.exception.TestException;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ProviderRepositoryTest {
|
||||
|
||||
private static final String DOMAIN_NAME = "domain name";
|
||||
|
|
@ -48,8 +47,9 @@ class ProviderRepositoryTest {
|
|||
@Test
|
||||
@DisplayName("should reject null as default provider")
|
||||
void shouldRejectNullAsDefaultProvider() {
|
||||
assertThatCode(() -> providerRepository.setProvider(null, mockAfterSet(), mockAfterInit(),
|
||||
mockAfterShutdown(), mockAfterError(), false)).isInstanceOf(IllegalArgumentException.class);
|
||||
assertThatCode(() -> providerRepository.setProvider(
|
||||
null, mockAfterSet(), mockAfterInit(), mockAfterShutdown(), mockAfterError(), false))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -64,13 +64,17 @@ class ProviderRepositoryTest {
|
|||
FeatureProvider featureProvider = createMockedProvider();
|
||||
doDelayResponse(Duration.ofSeconds(10)).when(featureProvider).initialize(new ImmutableContext());
|
||||
|
||||
await()
|
||||
.alias("wait for provider mutator to return")
|
||||
await().alias("wait for provider mutator to return")
|
||||
.pollDelay(Duration.ofMillis(1))
|
||||
.atMost(Duration.ofSeconds(1))
|
||||
.until(() -> {
|
||||
providerRepository.setProvider(featureProvider, mockAfterSet(), mockAfterInit(),
|
||||
mockAfterShutdown(), mockAfterError(), false);
|
||||
providerRepository.setProvider(
|
||||
featureProvider,
|
||||
mockAfterSet(),
|
||||
mockAfterInit(),
|
||||
mockAfterShutdown(),
|
||||
mockAfterError(),
|
||||
false);
|
||||
verify(featureProvider, timeout(TIMEOUT)).initialize(any());
|
||||
return true;
|
||||
});
|
||||
|
|
@ -85,8 +89,14 @@ class ProviderRepositoryTest {
|
|||
@Test
|
||||
@DisplayName("should reject null as named provider")
|
||||
void shouldRejectNullAsNamedProvider() {
|
||||
assertThatCode(() -> providerRepository.setProvider(DOMAIN_NAME, null, mockAfterSet(), mockAfterInit(),
|
||||
mockAfterShutdown(), mockAfterError(), false))
|
||||
assertThatCode(() -> providerRepository.setProvider(
|
||||
DOMAIN_NAME,
|
||||
null,
|
||||
mockAfterSet(),
|
||||
mockAfterInit(),
|
||||
mockAfterShutdown(),
|
||||
mockAfterError(),
|
||||
false))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
|
|
@ -94,8 +104,14 @@ class ProviderRepositoryTest {
|
|||
@DisplayName("should reject null as domain name")
|
||||
void shouldRejectNullAsDefaultProvider() {
|
||||
NoOpProvider provider = new NoOpProvider();
|
||||
assertThatCode(() -> providerRepository.setProvider(null, provider, mockAfterSet(), mockAfterInit(),
|
||||
mockAfterShutdown(), mockAfterError(), false))
|
||||
assertThatCode(() -> providerRepository.setProvider(
|
||||
null,
|
||||
provider,
|
||||
mockAfterSet(),
|
||||
mockAfterInit(),
|
||||
mockAfterShutdown(),
|
||||
mockAfterError(),
|
||||
false))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
|
|
@ -105,13 +121,18 @@ class ProviderRepositoryTest {
|
|||
FeatureProvider featureProvider = createMockedProvider();
|
||||
doDelayResponse(Duration.ofSeconds(10)).when(featureProvider).initialize(any());
|
||||
|
||||
await()
|
||||
.alias("wait for provider mutator to return")
|
||||
await().alias("wait for provider mutator to return")
|
||||
.pollDelay(Duration.ofMillis(1))
|
||||
.atMost(Duration.ofSeconds(1))
|
||||
.until(() -> {
|
||||
providerRepository.setProvider("a domain", featureProvider, mockAfterSet(),
|
||||
mockAfterInit(), mockAfterShutdown(), mockAfterError(), false);
|
||||
providerRepository.setProvider(
|
||||
"a domain",
|
||||
featureProvider,
|
||||
mockAfterSet(),
|
||||
mockAfterInit(),
|
||||
mockAfterShutdown(),
|
||||
mockAfterError(),
|
||||
false);
|
||||
verify(featureProvider, timeout(TIMEOUT)).initialize(any());
|
||||
return true;
|
||||
});
|
||||
|
|
@ -131,13 +152,17 @@ class ProviderRepositoryTest {
|
|||
FeatureProvider newProvider = createMockedProvider();
|
||||
doDelayResponse(Duration.ofSeconds(10)).when(newProvider).initialize(any());
|
||||
|
||||
await()
|
||||
.alias("wait for provider mutator to return")
|
||||
await().alias("wait for provider mutator to return")
|
||||
.pollDelay(Duration.ofMillis(1))
|
||||
.atMost(Duration.ofSeconds(1))
|
||||
.until(() -> {
|
||||
providerRepository.setProvider(newProvider, mockAfterSet(), mockAfterInit(),
|
||||
mockAfterShutdown(), mockAfterError(), false);
|
||||
providerRepository.setProvider(
|
||||
newProvider,
|
||||
mockAfterSet(),
|
||||
mockAfterInit(),
|
||||
mockAfterShutdown(),
|
||||
mockAfterError(),
|
||||
false);
|
||||
verify(newProvider, timeout(TIMEOUT)).initialize(any());
|
||||
return true;
|
||||
});
|
||||
|
|
@ -168,12 +193,16 @@ class ProviderRepositoryTest {
|
|||
FeatureProvider newProvider = createMockedProvider();
|
||||
doDelayResponse(Duration.ofSeconds(10)).when(newProvider).initialize(any());
|
||||
|
||||
Future<?> providerMutation = executorService
|
||||
.submit(() -> providerRepository.setProvider(DOMAIN_NAME, newProvider, mockAfterSet(),
|
||||
mockAfterInit(), mockAfterShutdown(), mockAfterError(), false));
|
||||
Future<?> providerMutation = executorService.submit(() -> providerRepository.setProvider(
|
||||
DOMAIN_NAME,
|
||||
newProvider,
|
||||
mockAfterSet(),
|
||||
mockAfterInit(),
|
||||
mockAfterShutdown(),
|
||||
mockAfterError(),
|
||||
false));
|
||||
|
||||
await()
|
||||
.alias("wait for provider mutator to return")
|
||||
await().alias("wait for provider mutator to return")
|
||||
.pollDelay(Duration.ofMillis(1))
|
||||
.atMost(Duration.ofSeconds(1))
|
||||
.until(providerMutation::isDone);
|
||||
|
|
@ -278,55 +307,47 @@ class ProviderRepositoryTest {
|
|||
}
|
||||
|
||||
private void setFeatureProvider(FeatureProvider provider) {
|
||||
providerRepository.setProvider(provider, mockAfterSet(), mockAfterInit(), mockAfterShutdown(),
|
||||
mockAfterError(), false);
|
||||
providerRepository.setProvider(
|
||||
provider, mockAfterSet(), mockAfterInit(), mockAfterShutdown(), mockAfterError(), false);
|
||||
waitForSettingProviderHasBeenCompleted(ProviderRepository::getProvider, provider);
|
||||
}
|
||||
|
||||
|
||||
private void setFeatureProvider(FeatureProvider provider, Consumer<FeatureProvider> afterSet,
|
||||
Consumer<FeatureProvider> afterInit, Consumer<FeatureProvider> afterShutdown,
|
||||
BiConsumer<FeatureProvider, OpenFeatureError> afterError) {
|
||||
providerRepository.setProvider(provider, afterSet, afterInit, afterShutdown,
|
||||
afterError, false);
|
||||
private void setFeatureProvider(
|
||||
FeatureProvider provider,
|
||||
Consumer<FeatureProvider> afterSet,
|
||||
Consumer<FeatureProvider> afterInit,
|
||||
Consumer<FeatureProvider> afterShutdown,
|
||||
BiConsumer<FeatureProvider, OpenFeatureError> afterError) {
|
||||
providerRepository.setProvider(provider, afterSet, afterInit, afterShutdown, afterError, false);
|
||||
waitForSettingProviderHasBeenCompleted(ProviderRepository::getProvider, provider);
|
||||
}
|
||||
|
||||
private void setFeatureProvider(String namedProvider, FeatureProvider provider) {
|
||||
providerRepository.setProvider(namedProvider, provider, mockAfterSet(), mockAfterInit(), mockAfterShutdown(),
|
||||
mockAfterError(), false);
|
||||
providerRepository.setProvider(
|
||||
namedProvider, provider, mockAfterSet(), mockAfterInit(), mockAfterShutdown(), mockAfterError(), false);
|
||||
waitForSettingProviderHasBeenCompleted(repository -> repository.getProvider(namedProvider), provider);
|
||||
}
|
||||
|
||||
private void waitForSettingProviderHasBeenCompleted(
|
||||
Function<ProviderRepository, FeatureProvider> extractor,
|
||||
FeatureProvider provider) {
|
||||
await()
|
||||
.pollDelay(Duration.ofMillis(1))
|
||||
.atMost(Duration.ofSeconds(5))
|
||||
.until(() -> {
|
||||
return extractor.apply(providerRepository).equals(provider);
|
||||
});
|
||||
Function<ProviderRepository, FeatureProvider> extractor, FeatureProvider provider) {
|
||||
await().pollDelay(Duration.ofMillis(1)).atMost(Duration.ofSeconds(5)).until(() -> {
|
||||
return extractor.apply(providerRepository).equals(provider);
|
||||
});
|
||||
}
|
||||
|
||||
private Consumer<FeatureProvider> mockAfterSet() {
|
||||
return fp -> {
|
||||
};
|
||||
return fp -> {};
|
||||
}
|
||||
|
||||
private Consumer<FeatureProvider> mockAfterInit() {
|
||||
return fp -> {
|
||||
};
|
||||
return fp -> {};
|
||||
}
|
||||
|
||||
private Consumer<FeatureProvider> mockAfterShutdown() {
|
||||
return fp -> {
|
||||
};
|
||||
return fp -> {};
|
||||
}
|
||||
|
||||
private BiConsumer<FeatureProvider, OpenFeatureError> mockAfterError() {
|
||||
return (fp, ex) -> {
|
||||
};
|
||||
return (fp, ex) -> {};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,17 +10,31 @@ import org.junit.jupiter.api.Test;
|
|||
public class ProviderSpecTest {
|
||||
NoOpProvider p = new NoOpProvider();
|
||||
|
||||
@Specification(number = "2.1.1", text = "The provider interface MUST define a metadata member or accessor, containing a name field or accessor of type string, which identifies the provider implementation.")
|
||||
@Specification(
|
||||
number = "2.1.1",
|
||||
text =
|
||||
"The provider interface MUST define a metadata member or accessor, containing a name field or accessor of type string, which identifies the provider implementation.")
|
||||
@Test
|
||||
void name_accessor() {
|
||||
assertNotNull(p.getName());
|
||||
}
|
||||
|
||||
@Specification(number = "2.2.2.1", text = "The feature provider interface MUST define methods for typed " +
|
||||
"flag resolution, including boolean, numeric, string, and structure.")
|
||||
@Specification(number = "2.2.3", text = "In cases of normal execution, the `provider` MUST populate the `resolution details` structure's `value` field with the resolved flag value.")
|
||||
@Specification(number = "2.2.1", text = "The `feature provider` interface MUST define methods to resolve flag values, with parameters `flag key` (string, required), `default value` (boolean | number | string | structure, required) and `evaluation context` (optional), which returns a `resolution details` structure.")
|
||||
@Specification(number = "2.2.8.1", text = "The `resolution details` structure SHOULD accept a generic argument (or use an equivalent language feature) which indicates the type of the wrapped `value` field.")
|
||||
@Specification(
|
||||
number = "2.2.2.1",
|
||||
text = "The feature provider interface MUST define methods for typed "
|
||||
+ "flag resolution, including boolean, numeric, string, and structure.")
|
||||
@Specification(
|
||||
number = "2.2.3",
|
||||
text =
|
||||
"In cases of normal execution, the `provider` MUST populate the `resolution details` structure's `value` field with the resolved flag value.")
|
||||
@Specification(
|
||||
number = "2.2.1",
|
||||
text =
|
||||
"The `feature provider` interface MUST define methods to resolve flag values, with parameters `flag key` (string, required), `default value` (boolean | number | string | structure, required) and `evaluation context` (optional), which returns a `resolution details` structure.")
|
||||
@Specification(
|
||||
number = "2.2.8.1",
|
||||
text =
|
||||
"The `resolution details` structure SHOULD accept a generic argument (or use an equivalent language feature) which indicates the type of the wrapped `value` field.")
|
||||
@Test
|
||||
void flag_value_set() {
|
||||
ProviderEvaluation<Integer> int_result = p.getIntegerEvaluation("key", 4, new ImmutableContext());
|
||||
|
|
@ -37,31 +51,47 @@ public class ProviderSpecTest {
|
|||
|
||||
ProviderEvaluation<Value> object_result = p.getObjectEvaluation("key", new Value(), new ImmutableContext());
|
||||
assertNotNull(object_result.getValue());
|
||||
|
||||
}
|
||||
|
||||
@Specification(number = "2.2.5", text = "The `provider` SHOULD populate the `resolution details` structure's `reason` field with `\"STATIC\"`, `\"DEFAULT\",` `\"TARGETING_MATCH\"`, `\"SPLIT\"`, `\"CACHED\"`, `\"DISABLED\"`, `\"UNKNOWN\"`, `\"STALE\"`, `\"ERROR\"` or some other string indicating the semantic reason for the returned flag value.")
|
||||
@Specification(
|
||||
number = "2.2.5",
|
||||
text =
|
||||
"The `provider` SHOULD populate the `resolution details` structure's `reason` field with `\"STATIC\"`, `\"DEFAULT\",` `\"TARGETING_MATCH\"`, `\"SPLIT\"`, `\"CACHED\"`, `\"DISABLED\"`, `\"UNKNOWN\"`, `\"STALE\"`, `\"ERROR\"` or some other string indicating the semantic reason for the returned flag value.")
|
||||
@Test
|
||||
void has_reason() {
|
||||
ProviderEvaluation<Boolean> result = p.getBooleanEvaluation("key", false, new ImmutableContext());
|
||||
assertEquals(Reason.DEFAULT.toString(), result.getReason());
|
||||
}
|
||||
|
||||
@Specification(number = "2.2.6", text = "In cases of normal execution, the `provider` MUST NOT populate the `resolution details` structure's `error code` field, or otherwise must populate it with a null or falsy value.")
|
||||
@Specification(
|
||||
number = "2.2.6",
|
||||
text =
|
||||
"In cases of normal execution, the `provider` MUST NOT populate the `resolution details` structure's `error code` field, or otherwise must populate it with a null or falsy value.")
|
||||
@Test
|
||||
void no_error_code_by_default() {
|
||||
ProviderEvaluation<Boolean> result = p.getBooleanEvaluation("key", false, new ImmutableContext());
|
||||
assertNull(result.getErrorCode());
|
||||
}
|
||||
|
||||
@Specification(number = "2.2.7", text = "In cases of abnormal execution, the `provider` **MUST** indicate an error using the idioms of the implementation language, with an associated `error code` and optional associated `error message`.")
|
||||
@Specification(number = "2.3.2", text = "In cases of normal execution, the `provider` MUST NOT populate the `resolution details` structure's `error message` field, or otherwise must populate it with a null or falsy value.")
|
||||
@Specification(number = "2.3.3", text = "In cases of abnormal execution, the `resolution details` structure's `error message` field MAY contain a string containing additional detail about the nature of the error.")
|
||||
@Specification(
|
||||
number = "2.2.7",
|
||||
text =
|
||||
"In cases of abnormal execution, the `provider` **MUST** indicate an error using the idioms of the implementation language, with an associated `error code` and optional associated `error message`.")
|
||||
@Specification(
|
||||
number = "2.3.2",
|
||||
text =
|
||||
"In cases of normal execution, the `provider` MUST NOT populate the `resolution details` structure's `error message` field, or otherwise must populate it with a null or falsy value.")
|
||||
@Specification(
|
||||
number = "2.3.3",
|
||||
text =
|
||||
"In cases of abnormal execution, the `resolution details` structure's `error message` field MAY contain a string containing additional detail about the nature of the error.")
|
||||
@Test
|
||||
void up_to_provider_implementation() {
|
||||
}
|
||||
void up_to_provider_implementation() {}
|
||||
|
||||
@Specification(number = "2.2.4", text = "In cases of normal execution, the `provider` SHOULD populate the `resolution details` structure's `variant` field with a string identifier corresponding to the returned flag value.")
|
||||
@Specification(
|
||||
number = "2.2.4",
|
||||
text =
|
||||
"In cases of normal execution, the `provider` SHOULD populate the `resolution details` structure's `variant` field with a string identifier corresponding to the returned flag value.")
|
||||
@Test
|
||||
void variant_set() {
|
||||
ProviderEvaluation<Integer> int_result = p.getIntegerEvaluation("key", 4, new ImmutableContext());
|
||||
|
|
@ -77,7 +107,10 @@ public class ProviderSpecTest {
|
|||
assertNotNull(boolean_result.getReason());
|
||||
}
|
||||
|
||||
@Specification(number = "2.2.10", text = "`flag metadata` MUST be a structure supporting the definition of arbitrary properties, with keys of type `string`, and values of type `boolean | string | number`.")
|
||||
@Specification(
|
||||
number = "2.2.10",
|
||||
text =
|
||||
"`flag metadata` MUST be a structure supporting the definition of arbitrary properties, with keys of type `string`, and values of type `boolean | string | number`.")
|
||||
@Test
|
||||
void flag_metadata_structure() {
|
||||
ImmutableMetadata metadata = ImmutableMetadata.builder()
|
||||
|
|
@ -97,30 +130,51 @@ public class ProviderSpecTest {
|
|||
assertEquals("str", metadata.getString("string"));
|
||||
}
|
||||
|
||||
@Specification(number = "2.3.1", text = "The provider interface MUST define a provider hook mechanism which can be optionally implemented in order to add hook instances to the evaluation life-cycle.")
|
||||
@Specification(number = "4.4.1", text = "The API, Client, Provider, and invocation MUST have a method for registering hooks.")
|
||||
@Specification(
|
||||
number = "2.3.1",
|
||||
text =
|
||||
"The provider interface MUST define a provider hook mechanism which can be optionally implemented in order to add hook instances to the evaluation life-cycle.")
|
||||
@Specification(
|
||||
number = "4.4.1",
|
||||
text = "The API, Client, Provider, and invocation MUST have a method for registering hooks.")
|
||||
@Test
|
||||
void provider_hooks() {
|
||||
assertEquals(0, p.getProviderHooks().size());
|
||||
}
|
||||
|
||||
@Specification(number = "2.4.2", text = "The provider MAY define a status field/accessor which indicates the readiness of the provider, with possible values NOT_READY, READY, or ERROR.")
|
||||
@Specification(
|
||||
number = "2.4.2",
|
||||
text =
|
||||
"The provider MAY define a status field/accessor which indicates the readiness of the provider, with possible values NOT_READY, READY, or ERROR.")
|
||||
@Test
|
||||
void defines_status() {
|
||||
assertTrue(p.getState() instanceof ProviderState);
|
||||
}
|
||||
|
||||
@Specification(number = "2.4.3", text = "The provider MUST set its status field/accessor to READY if its initialize function terminates normally.")
|
||||
@Specification(number = "2.4.4", text = "The provider MUST set its status field to ERROR if its initialize function terminates abnormally.")
|
||||
@Specification(number = "2.2.9", text = "The provider SHOULD populate the resolution details structure's flag metadata field.")
|
||||
@Specification(number = "2.4.1", text = "The provider MAY define an initialize function which accepts the global evaluation context as an argument and performs initialization logic relevant to the provider.")
|
||||
@Specification(number = "2.5.1", text = "The provider MAY define a mechanism to gracefully shutdown and dispose of resources.")
|
||||
@Specification(
|
||||
number = "2.4.3",
|
||||
text =
|
||||
"The provider MUST set its status field/accessor to READY if its initialize function terminates normally.")
|
||||
@Specification(
|
||||
number = "2.4.4",
|
||||
text = "The provider MUST set its status field to ERROR if its initialize function terminates abnormally.")
|
||||
@Specification(
|
||||
number = "2.2.9",
|
||||
text = "The provider SHOULD populate the resolution details structure's flag metadata field.")
|
||||
@Specification(
|
||||
number = "2.4.1",
|
||||
text =
|
||||
"The provider MAY define an initialize function which accepts the global evaluation context as an argument and performs initialization logic relevant to the provider.")
|
||||
@Specification(
|
||||
number = "2.5.1",
|
||||
text = "The provider MAY define a mechanism to gracefully shutdown and dispose of resources.")
|
||||
@Test
|
||||
void provider_responsibility() {
|
||||
}
|
||||
void provider_responsibility() {}
|
||||
|
||||
@Specification(number = "2.6.1", text = "The provider MAY define an on context changed handler, which takes an argument for the previous context and the newly set context, in order to respond to an evaluation context change.")
|
||||
@Specification(
|
||||
number = "2.6.1",
|
||||
text =
|
||||
"The provider MAY define an on context changed handler, which takes an argument for the previous context and the newly set context, in order to respond to an evaluation context change.")
|
||||
@Test
|
||||
void not_applicable_for_dynamic_context() {
|
||||
}
|
||||
void not_applicable_for_dynamic_context() {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,17 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import static dev.openfeature.sdk.testutils.FeatureProviderTestUtils.setFeatureProvider;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import dev.openfeature.sdk.fixtures.ProviderFixture;
|
||||
import dev.openfeature.sdk.testutils.exception.TestException;
|
||||
import java.time.Duration;
|
||||
import org.awaitility.Awaitility;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import static dev.openfeature.sdk.testutils.FeatureProviderTestUtils.setFeatureProvider;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class ShutdownBehaviorSpecTest {
|
||||
|
||||
private String DOMAIN = "myDomain";
|
||||
|
|
@ -25,9 +24,13 @@ class ShutdownBehaviorSpecTest {
|
|||
@Nested
|
||||
class DefaultProvider {
|
||||
|
||||
@Specification(number = "1.1.2.3", text = "The `provider mutator` function MUST invoke the `shutdown` function on the previously registered provider once it's no longer being used to resolve flag values.")
|
||||
@Specification(
|
||||
number = "1.1.2.3",
|
||||
text =
|
||||
"The `provider mutator` function MUST invoke the `shutdown` function on the previously registered provider once it's no longer being used to resolve flag values.")
|
||||
@Test
|
||||
@DisplayName("must invoke shutdown method on previously registered provider once it should not be used for flag evaluation anymore")
|
||||
@DisplayName(
|
||||
"must invoke shutdown method on previously registered provider once it should not be used for flag evaluation anymore")
|
||||
void mustInvokeShutdownMethodOnPreviouslyRegisteredProviderOnceItShouldNotBeUsedForFlagEvaluationAnymore() {
|
||||
FeatureProvider featureProvider = ProviderFixture.createMockedProvider();
|
||||
|
||||
|
|
@ -37,10 +40,12 @@ class ShutdownBehaviorSpecTest {
|
|||
verify(featureProvider, timeout(1000)).shutdown();
|
||||
}
|
||||
|
||||
@Specification(number = "1.4.10", text = "Methods, functions, or operations on the client MUST NOT throw "
|
||||
+ "exceptions, or otherwise abnormally terminate. Flag evaluation calls must always return the "
|
||||
+ "`default value` in the event of abnormal execution. Exceptions include functions or methods for "
|
||||
+ "the purposes for configuration or setup.")
|
||||
@Specification(
|
||||
number = "1.4.10",
|
||||
text = "Methods, functions, or operations on the client MUST NOT throw "
|
||||
+ "exceptions, or otherwise abnormally terminate. Flag evaluation calls must always return the "
|
||||
+ "`default value` in the event of abnormal execution. Exceptions include functions or methods for "
|
||||
+ "the purposes for configuration or setup.")
|
||||
@Test
|
||||
@DisplayName("should catch exception thrown by the provider on shutdown")
|
||||
void shouldCatchExceptionThrownByTheProviderOnShutdown() {
|
||||
|
|
@ -57,9 +62,13 @@ class ShutdownBehaviorSpecTest {
|
|||
@Nested
|
||||
class NamedProvider {
|
||||
|
||||
@Specification(number = "1.1.2.3", text = "The `provider mutator` function MUST invoke the `shutdown` function on the previously registered provider once it's no longer being used to resolve flag values.")
|
||||
@Specification(
|
||||
number = "1.1.2.3",
|
||||
text =
|
||||
"The `provider mutator` function MUST invoke the `shutdown` function on the previously registered provider once it's no longer being used to resolve flag values.")
|
||||
@Test
|
||||
@DisplayName("must invoke shutdown method on previously registered provider once it should not be used for flag evaluation anymore")
|
||||
@DisplayName(
|
||||
"must invoke shutdown method on previously registered provider once it should not be used for flag evaluation anymore")
|
||||
void mustInvokeShutdownMethodOnPreviouslyRegisteredProviderOnceItShouldNotBeUsedForFlagEvaluationAnymore() {
|
||||
FeatureProvider featureProvider = ProviderFixture.createMockedProvider();
|
||||
|
||||
|
|
@ -69,10 +78,12 @@ class ShutdownBehaviorSpecTest {
|
|||
verify(featureProvider, timeout(1000)).shutdown();
|
||||
}
|
||||
|
||||
@Specification(number = "1.4.10", text = "Methods, functions, or operations on the client MUST NOT throw "
|
||||
+ "exceptions, or otherwise abnormally terminate. Flag evaluation calls must always return the "
|
||||
+ "`default value` in the event of abnormal execution. Exceptions include functions or methods for "
|
||||
+ "the purposes for configuration or setup.")
|
||||
@Specification(
|
||||
number = "1.4.10",
|
||||
text = "Methods, functions, or operations on the client MUST NOT throw "
|
||||
+ "exceptions, or otherwise abnormally terminate. Flag evaluation calls must always return the "
|
||||
+ "`default value` in the event of abnormal execution. Exceptions include functions or methods for "
|
||||
+ "the purposes for configuration or setup.")
|
||||
@Test
|
||||
@DisplayName("should catch exception thrown by the named client provider on shutdown")
|
||||
void shouldCatchExceptionThrownByTheNamedClientProviderOnShutdown() {
|
||||
|
|
@ -89,7 +100,9 @@ class ShutdownBehaviorSpecTest {
|
|||
@Nested
|
||||
class General {
|
||||
|
||||
@Specification(number = "1.6.1", text = "The API MUST define a mechanism to propagate a shutdown request to active providers.")
|
||||
@Specification(
|
||||
number = "1.6.1",
|
||||
text = "The API MUST define a mechanism to propagate a shutdown request to active providers.")
|
||||
@Test
|
||||
@DisplayName("must shutdown all providers on shutting down api")
|
||||
void mustShutdownAllProvidersOnShuttingDownApi() {
|
||||
|
|
@ -102,17 +115,13 @@ class ShutdownBehaviorSpecTest {
|
|||
synchronized (OpenFeatureAPI.class) {
|
||||
api.shutdown();
|
||||
|
||||
Awaitility
|
||||
.await()
|
||||
.atMost(Duration.ofSeconds(1))
|
||||
.untilAsserted(() -> {
|
||||
verify(defaultProvider).shutdown();
|
||||
verify(namedProvider).shutdown();
|
||||
});
|
||||
Awaitility.await().atMost(Duration.ofSeconds(1)).untilAsserted(() -> {
|
||||
verify(defaultProvider).shutdown();
|
||||
verify(namedProvider).shutdown();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
@DisplayName("once shutdown is complete, api must be ready to use again")
|
||||
void apiIsReadyToUseAfterShutdown() {
|
||||
|
|
|
|||
|
|
@ -5,5 +5,6 @@ import java.lang.annotation.Repeatable;
|
|||
@Repeatable(Specifications.class)
|
||||
public @interface Specification {
|
||||
String number();
|
||||
|
||||
String text();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,29 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static dev.openfeature.sdk.Structure.mapToStructure;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.SneakyThrows;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class StructureTest {
|
||||
@Test public void noArgShouldContainEmptyAttributes() {
|
||||
@Test
|
||||
public void noArgShouldContainEmptyAttributes() {
|
||||
MutableStructure structure = new MutableStructure();
|
||||
assertEquals(0, structure.asMap().keySet().size());
|
||||
}
|
||||
|
||||
@Test public void mapArgShouldContainNewMap() {
|
||||
@Test
|
||||
public void mapArgShouldContainNewMap() {
|
||||
String KEY = "key";
|
||||
Map<String, Value> map = new HashMap<String, Value>() {
|
||||
{
|
||||
|
|
@ -34,7 +35,8 @@ public class StructureTest {
|
|||
assertNotSame(structure.asMap(), map); // should be a copy
|
||||
}
|
||||
|
||||
@Test public void addAndGetAddAndReturnValues() {
|
||||
@Test
|
||||
public void addAndGetAddAndReturnValues() {
|
||||
String BOOL_KEY = "bool";
|
||||
String STRING_KEY = "string";
|
||||
String INT_KEY = "int";
|
||||
|
|
@ -104,7 +106,7 @@ public class StructureTest {
|
|||
@Test
|
||||
void asObjectHandlesNullValue() {
|
||||
Map<String, Value> map = new HashMap<>();
|
||||
map.put("null", new Value((String)null));
|
||||
map.put("null", new Value((String) null));
|
||||
ImmutableStructure structure = new ImmutableStructure(map);
|
||||
assertNull(structure.asObjectMap().get("null"));
|
||||
}
|
||||
|
|
@ -112,6 +114,6 @@ public class StructureTest {
|
|||
@Test
|
||||
void convertValueHandlesNullValue() {
|
||||
ImmutableStructure structure = new ImmutableStructure();
|
||||
assertNull(structure.convertValue(new Value((String)null)));
|
||||
assertNull(structure.convertValue(new Value((String) null)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.FutureTask;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import lombok.SneakyThrows;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class ThreadLocalTransactionContextPropagatorTest {
|
||||
|
||||
|
|
@ -54,4 +53,4 @@ public class ThreadLocalTransactionContextPropagatorTest {
|
|||
assertSame(secondContext, secondThreadContext);
|
||||
assertSame(firstContext, contextPropagator.getTransactionContext());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,5 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Maps;
|
||||
import dev.openfeature.sdk.fixtures.ProviderFixture;
|
||||
import dev.openfeature.sdk.testutils.FeatureProviderTestUtils;
|
||||
import lombok.SneakyThrows;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
|
@ -23,6 +12,16 @@ import static org.mockito.Mockito.argThat;
|
|||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Maps;
|
||||
import dev.openfeature.sdk.fixtures.ProviderFixture;
|
||||
import dev.openfeature.sdk.testutils.FeatureProviderTestUtils;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import lombok.SneakyThrows;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class TrackingSpecTest {
|
||||
|
||||
private OpenFeatureAPI api;
|
||||
|
|
@ -34,13 +33,16 @@ class TrackingSpecTest {
|
|||
client = api.getClient();
|
||||
}
|
||||
|
||||
|
||||
@Specification(number = "6.1.1.1", text = "The `client` MUST define a function for tracking the occurrence of " +
|
||||
"a particular action or application state, with parameters `tracking event name` (string, required), " +
|
||||
"`evaluation context` (optional) and `tracking event details` (optional), which returns nothing.")
|
||||
@Specification(number = "6.1.2.1", text = "The `client` MUST define a function for tracking the occurrence of a " +
|
||||
"particular action or application state, with parameters `tracking event name` (string, required) and " +
|
||||
"`tracking event details` (optional), which returns nothing.")
|
||||
@Specification(
|
||||
number = "6.1.1.1",
|
||||
text = "The `client` MUST define a function for tracking the occurrence of "
|
||||
+ "a particular action or application state, with parameters `tracking event name` (string, required), "
|
||||
+ "`evaluation context` (optional) and `tracking event details` (optional), which returns nothing.")
|
||||
@Specification(
|
||||
number = "6.1.2.1",
|
||||
text = "The `client` MUST define a function for tracking the occurrence of a "
|
||||
+ "particular action or application state, with parameters `tracking event name` (string, required) and "
|
||||
+ "`tracking event details` (optional), which returns nothing.")
|
||||
@Test
|
||||
@SneakyThrows
|
||||
void trackMethodFulfillsSpec() {
|
||||
|
|
@ -65,7 +67,6 @@ class TrackingSpecTest {
|
|||
assertThrows(IllegalArgumentException.class, () -> client.track("", ctx));
|
||||
assertThrows(IllegalArgumentException.class, () -> client.track("", ctx, details));
|
||||
|
||||
|
||||
Class<OpenFeatureClient> clientClass = OpenFeatureClient.class;
|
||||
assertEquals(
|
||||
void.class,
|
||||
|
|
@ -73,20 +74,24 @@ class TrackingSpecTest {
|
|||
"The method should return void.");
|
||||
assertEquals(
|
||||
void.class,
|
||||
clientClass.getMethod("track", String.class, EvaluationContext.class).getReturnType(),
|
||||
clientClass
|
||||
.getMethod("track", String.class, EvaluationContext.class)
|
||||
.getReturnType(),
|
||||
"The method should return void.");
|
||||
|
||||
assertEquals(
|
||||
void.class,
|
||||
clientClass.getMethod("track", String.class, EvaluationContext.class, TrackingEventDetails.class).getReturnType(),
|
||||
clientClass
|
||||
.getMethod("track", String.class, EvaluationContext.class, TrackingEventDetails.class)
|
||||
.getReturnType(),
|
||||
"The method should return void.");
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Specification(number = "6.1.3", text = "The evaluation context passed to the provider's track function " +
|
||||
"MUST be merged in the order: API (global; lowest precedence) -> transaction -> client -> " +
|
||||
"invocation (highest precedence), with duplicate values being overwritten.")
|
||||
@Specification(
|
||||
number = "6.1.3",
|
||||
text = "The evaluation context passed to the provider's track function "
|
||||
+ "MUST be merged in the order: API (global; lowest precedence) -> transaction -> client -> "
|
||||
+ "invocation (highest precedence), with duplicate values being overwritten.")
|
||||
@Test
|
||||
void contextsGetMerged() {
|
||||
|
||||
|
|
@ -123,8 +128,10 @@ class TrackingSpecTest {
|
|||
verify(provider).track(eq("event"), argThat(ctx -> ctx.asMap().equals(expectedMap)), notNull());
|
||||
}
|
||||
|
||||
@Specification(number = "6.1.4", text = "If the client's `track` function is called and the associated provider " +
|
||||
"does not implement tracking, the client's `track` function MUST no-op.")
|
||||
@Specification(
|
||||
number = "6.1.4",
|
||||
text = "If the client's `track` function is called and the associated provider "
|
||||
+ "does not implement tracking, the client's `track` function MUST no-op.")
|
||||
@Test
|
||||
void noopProvider() {
|
||||
FeatureProvider provider = spy(FeatureProvider.class);
|
||||
|
|
@ -133,10 +140,15 @@ class TrackingSpecTest {
|
|||
verify(provider).track(any(), any(), any());
|
||||
}
|
||||
|
||||
@Specification(number = "6.2.1", text = "The `tracking event details` structure MUST define an optional numeric " +
|
||||
"`value`, associating a scalar quality with an `tracking event`.")
|
||||
@Specification(number = "6.2.2", text = "The `tracking event details` MUST support the inclusion of custom " +
|
||||
"fields, having keys of type `string`, and values of type `boolean | string | number | structure`.")
|
||||
@Specification(
|
||||
number = "6.2.1",
|
||||
text = "The `tracking event details` structure MUST define an optional numeric "
|
||||
+ "`value`, associating a scalar quality with an `tracking event`.")
|
||||
@Specification(
|
||||
number = "6.2.2",
|
||||
text =
|
||||
"The `tracking event details` MUST support the inclusion of custom "
|
||||
+ "fields, having keys of type `string`, and values of type `boolean | string | number | structure`.")
|
||||
@Test
|
||||
void eventDetails() {
|
||||
assertFalse(new MutableTrackingEventDetails().getValue().isPresent());
|
||||
|
|
@ -144,7 +156,6 @@ class TrackingSpecTest {
|
|||
assertThat(new ImmutableTrackingEventDetails(2).getValue()).hasValue(2);
|
||||
assertThat(new MutableTrackingEventDetails(9.87f).getValue()).hasValue(9.87f);
|
||||
|
||||
|
||||
// using mutable tracking event details
|
||||
Map<String, Value> expectedMap = Maps.newHashMap();
|
||||
expectedMap.put("my-str", new Value("str"));
|
||||
|
|
@ -159,31 +170,27 @@ class TrackingSpecTest {
|
|||
.add("my-struct", new Value(new MutableTrackingEventDetails()));
|
||||
|
||||
assertEquals(expectedMap, details.asMap());
|
||||
assertThatCode(() -> OpenFeatureAPI.getInstance().getClient().
|
||||
track(
|
||||
"tracking-event-name",
|
||||
new ImmutableContext(),
|
||||
new MutableTrackingEventDetails()))
|
||||
assertThatCode(() -> OpenFeatureAPI.getInstance()
|
||||
.getClient()
|
||||
.track("tracking-event-name", new ImmutableContext(), new MutableTrackingEventDetails()))
|
||||
.doesNotThrowAnyException();
|
||||
|
||||
|
||||
// using immutable tracking event details
|
||||
ImmutableMap<String, Value> expectedImmutable = ImmutableMap.of("my-str", new Value("str"),
|
||||
"my-num", new Value(1),
|
||||
"my-bool", new Value(true),
|
||||
"my-struct", new Value(new ImmutableStructure())
|
||||
);
|
||||
ImmutableMap<String, Value> expectedImmutable = ImmutableMap.of(
|
||||
"my-str",
|
||||
new Value("str"),
|
||||
"my-num",
|
||||
new Value(1),
|
||||
"my-bool",
|
||||
new Value(true),
|
||||
"my-struct",
|
||||
new Value(new ImmutableStructure()));
|
||||
|
||||
ImmutableTrackingEventDetails immutableDetails = new ImmutableTrackingEventDetails(2, expectedMap);
|
||||
assertEquals(expectedImmutable, immutableDetails.asMap());
|
||||
assertThatCode(() -> OpenFeatureAPI.getInstance().getClient().
|
||||
track(
|
||||
"tracking-event-name",
|
||||
new ImmutableContext(),
|
||||
new ImmutableTrackingEventDetails()))
|
||||
assertThatCode(() -> OpenFeatureAPI.getInstance()
|
||||
.getClient()
|
||||
.track("tracking-event-name", new ImmutableContext(), new ImmutableTrackingEventDetails()))
|
||||
.doesNotThrowAnyException();
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,16 +9,17 @@ import static org.junit.jupiter.api.Assertions.fail;
|
|||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class ValueTest {
|
||||
@Test public void noArgShouldContainNull() {
|
||||
@Test
|
||||
public void noArgShouldContainNull() {
|
||||
Value value = new Value();
|
||||
assertTrue(value.isNull());
|
||||
}
|
||||
|
||||
@Test public void objectArgShouldContainObject() {
|
||||
@Test
|
||||
public void objectArgShouldContainObject() {
|
||||
try {
|
||||
// int is a special case, see intObjectArgShouldConvertToInt()
|
||||
List<Object> list = new ArrayList<>();
|
||||
|
|
@ -30,7 +31,7 @@ public class ValueTest {
|
|||
list.add(Instant.now());
|
||||
|
||||
int i = 0;
|
||||
for (Object l: list) {
|
||||
for (Object l : list) {
|
||||
Value value = new Value(l);
|
||||
assertEquals(list.get(i), value.asObject());
|
||||
i++;
|
||||
|
|
@ -40,7 +41,8 @@ public class ValueTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test public void intObjectArgShouldConvertToInt() {
|
||||
@Test
|
||||
public void intObjectArgShouldConvertToInt() {
|
||||
try {
|
||||
Object innerValue = 1;
|
||||
Value value = new Value(innerValue);
|
||||
|
|
@ -50,7 +52,8 @@ public class ValueTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test public void invalidObjectArgShouldThrow() {
|
||||
@Test
|
||||
public void invalidObjectArgShouldThrow() {
|
||||
|
||||
class Something {}
|
||||
|
||||
|
|
@ -59,18 +62,20 @@ public class ValueTest {
|
|||
});
|
||||
}
|
||||
|
||||
@Test public void boolArgShouldContainBool() {
|
||||
@Test
|
||||
public void boolArgShouldContainBool() {
|
||||
boolean innerValue = true;
|
||||
Value value = new Value(innerValue);
|
||||
assertTrue(value.isBoolean());
|
||||
assertEquals(innerValue, value.asBoolean());
|
||||
}
|
||||
|
||||
@Test public void numericArgShouldReturnDoubleOrInt() {
|
||||
@Test
|
||||
public void numericArgShouldReturnDoubleOrInt() {
|
||||
double innerDoubleValue = 1.75;
|
||||
Value doubleValue = new Value(innerDoubleValue);
|
||||
assertTrue(doubleValue.isNumber());
|
||||
assertEquals(1, doubleValue.asInteger()); // the double value represented by this object converted to type int
|
||||
assertEquals(1, doubleValue.asInteger()); // the double value represented by this object converted to type int
|
||||
assertEquals(1.75, doubleValue.asDouble());
|
||||
|
||||
int innerIntValue = 100;
|
||||
|
|
@ -80,21 +85,24 @@ public class ValueTest {
|
|||
assertEquals(innerIntValue, intValue.asDouble());
|
||||
}
|
||||
|
||||
@Test public void stringArgShouldContainString() {
|
||||
@Test
|
||||
public void stringArgShouldContainString() {
|
||||
String innerValue = "hi!";
|
||||
Value value = new Value(innerValue);
|
||||
assertTrue(value.isString());
|
||||
assertEquals(innerValue, value.asString());
|
||||
}
|
||||
|
||||
@Test public void dateShouldContainDate() {
|
||||
@Test
|
||||
public void dateShouldContainDate() {
|
||||
Instant innerValue = Instant.now();
|
||||
Value value = new Value(innerValue);
|
||||
assertTrue(value.isInstant());
|
||||
assertEquals(innerValue, value.asInstant());
|
||||
}
|
||||
|
||||
@Test public void structureShouldContainStructure() {
|
||||
@Test
|
||||
public void structureShouldContainStructure() {
|
||||
String INNER_KEY = "key";
|
||||
String INNER_VALUE = "val";
|
||||
MutableStructure innerValue = new MutableStructure().add(INNER_KEY, INNER_VALUE);
|
||||
|
|
@ -103,7 +111,8 @@ public class ValueTest {
|
|||
assertEquals(INNER_VALUE, value.asStructure().getValue(INNER_KEY).asString());
|
||||
}
|
||||
|
||||
@Test public void listArgShouldContainList() {
|
||||
@Test
|
||||
public void listArgShouldContainList() {
|
||||
String ITEM_VALUE = "val";
|
||||
List<Value> innerValue = new ArrayList<Value>();
|
||||
innerValue.add(new Value(ITEM_VALUE));
|
||||
|
|
@ -112,7 +121,8 @@ public class ValueTest {
|
|||
assertEquals(ITEM_VALUE, value.asList().get(0).asString());
|
||||
}
|
||||
|
||||
@Test public void listMustBeOfValues() {
|
||||
@Test
|
||||
public void listMustBeOfValues() {
|
||||
String item = "item";
|
||||
List<String> list = new ArrayList<>();
|
||||
list.add(item);
|
||||
|
|
@ -124,7 +134,8 @@ public class ValueTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test public void emptyListAllowed() {
|
||||
@Test
|
||||
public void emptyListAllowed() {
|
||||
List<String> list = new ArrayList<>();
|
||||
try {
|
||||
Value value = new Value((Object) list);
|
||||
|
|
@ -136,15 +147,17 @@ public class ValueTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test public void valueConstructorValidateListInternals() {
|
||||
@Test
|
||||
public void valueConstructorValidateListInternals() {
|
||||
List<Object> list = new ArrayList<>();
|
||||
list.add(new Value("item"));
|
||||
list.add("item");
|
||||
|
||||
assertThrows(InstantiationException.class, ()-> new Value(list));
|
||||
assertThrows(InstantiationException.class, () -> new Value(list));
|
||||
}
|
||||
|
||||
@Test public void noOpFinalize() {
|
||||
@Test
|
||||
public void noOpFinalize() {
|
||||
Value val = new Value();
|
||||
assertDoesNotThrow(val::finalize); // does nothing, but we want to defined in and make it final.
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue