diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4ca916ee..3130561f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -43,6 +43,17 @@ jobs: - run: bundle install - name: Rubocop run: bundle exec rubocop --color + steep: + runs-on: ubuntu-latest + name: Steep + steps: + - uses: actions/checkout@v3 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.2 + - run: bundle install + - name: Steep + run: bundle exec steep check # check the status of other jobs, so we can have a single job dependency for branch protection # https://github.com/community/community/discussions/4324#discussioncomment-3477871 status: diff --git a/Gemfile.lock b/Gemfile.lock index 5817217a..69a1bd5f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -6,16 +6,38 @@ PATH GEM remote: https://rubygems.org/ specs: + activesupport (7.0.8) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) ast (2.4.2) concurrent-ruby (1.2.2) + csv (3.2.7) diff-lcs (1.5.0) + ffi (1.15.5) + ffi (1.15.5-x64-mingw-ucrt) + ffi (1.15.5-x64-mingw32) + fileutils (1.7.1) + i18n (1.14.1) + concurrent-ruby (~> 1.0) json (2.6.2) + language_server-protocol (3.17.0.3) + listen (3.8.0) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + logger (1.5.3) markly (0.7.0) + minitest (5.20.0) parallel (1.22.1) parser (3.1.2.1) ast (~> 2.4.1) rainbow (3.1.1) rake (13.0.6) + rb-fsevent (0.11.2) + rb-inotify (0.10.1) + ffi (~> 1.0) + rbs (3.2.1) regexp_parser (2.6.0) rexml (3.2.5) rspec (3.12.0) @@ -44,6 +66,27 @@ GEM rubocop-ast (1.23.0) parser (>= 3.1.1.0) ruby-progressbar (1.11.0) + securerandom (0.2.2) + steep (1.5.3) + activesupport (>= 5.1) + concurrent-ruby (>= 1.1.10) + csv (>= 3.0.9) + fileutils (>= 1.1.0) + json (>= 2.1.0) + language_server-protocol (>= 3.15, < 4.0) + listen (~> 3.0) + logger (>= 1.3.0) + parser (>= 3.1) + rainbow (>= 2.2.2, < 4.0) + rbs (>= 3.1.0) + securerandom (>= 0.1) + strscan (>= 1.0.0) + terminal-table (>= 2, < 4) + strscan (3.0.6) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) unicode-display_width (2.3.0) PLATFORMS @@ -63,6 +106,7 @@ DEPENDENCIES rake (~> 13.0) rspec (~> 3.12.0) rubocop (~> 1.37.1) + steep (~> 1.5.0) BUNDLED WITH 2.3.25 diff --git a/Steepfile b/Steepfile new file mode 100644 index 00000000..4c720955 --- /dev/null +++ b/Steepfile @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +target :lib do + signature "sig" + check "lib" + library "pathname" + library "forwardable" + library "singleton" + library "uri" +end diff --git a/lib/openfeature/sdk/metadata.rb b/lib/openfeature/sdk/metadata.rb index 411ae1d5..e9bf2423 100644 --- a/lib/openfeature/sdk/metadata.rb +++ b/lib/openfeature/sdk/metadata.rb @@ -26,7 +26,7 @@ def initialize(name:, version: nil) end def ==(other) - raise ArgumentError("Expected comparison to be between Metadata object") unless other.is_a?(Metadata) + raise ArgumentError, "Expected comparison to be between Metadata object" unless other.is_a?(Metadata) @name == other.name && @version == other.version end diff --git a/openfeature-sdk.gemspec b/openfeature-sdk.gemspec index f507b227..3f38eecf 100644 --- a/openfeature-sdk.gemspec +++ b/openfeature-sdk.gemspec @@ -35,4 +35,5 @@ Gem::Specification.new do |spec| spec.add_development_dependency "rake", "~> 13.0" spec.add_development_dependency "rspec", "~> 3.12.0" spec.add_development_dependency "rubocop", "~> 1.37.1" + spec.add_development_dependency "steep", "~> 1.5.0" end diff --git a/sig/openfeature/sdk.rbs b/sig/openfeature/sdk.rbs index e058802f..863cd7ba 100644 --- a/sig/openfeature/sdk.rbs +++ b/sig/openfeature/sdk.rbs @@ -1,5 +1,5 @@ -module Openfeature - module Sdk +module OpenFeature + module SDK VERSION: String # See the writing guide of rbs: https://github.com/ruby/rbs#guides end diff --git a/sig/openfeature/sdk/_hook.rbs b/sig/openfeature/sdk/_hook.rbs new file mode 100644 index 00000000..f82584c8 --- /dev/null +++ b/sig/openfeature/sdk/_hook.rbs @@ -0,0 +1,31 @@ +type flagValue = bool | Numeric | String | Hash[String, untyped] +type flagValueType = :bool | :integer | :float | :string | :object +type evaluationContext = { targeting_key: String? } +type evaluationDetails = { flag_key: String, flag_value?: flagValue } + +type hookContext[T] = { + flag_key: String, + flag_value_type: flagValueType, + evaluation_context: evaluationContext, + default_value: T, + } + +type hookHints = Hash[String, flagValue] + +interface _BeforeHook + def before: (evaluationContext, ?hookHints) -> (evaluationContext | void) +end + +interface _AfterHook[T] + def after: (hookContext[T], evaluationDetails, ?hookHints) -> void +end + +interface _ErrorHook[T] + def error: (hookContext[T], Exception, ?hookHints) -> void +end + +interface _FinallyHook[T] + def finally: (hookContext[T], ?hookHints) -> void +end + +type hook = _BeforeHook | _AfterHook[flagValue] | _ErrorHook[flagValue] | _FinallyHook[flagValue] diff --git a/sig/openfeature/sdk/api.rbs b/sig/openfeature/sdk/api.rbs new file mode 100644 index 00000000..61bf5037 --- /dev/null +++ b/sig/openfeature/sdk/api.rbs @@ -0,0 +1,22 @@ +module OpenFeature + module SDK + + interface _Configuration + def context: () -> void + end + + class API + include _Configuration + extend Forwardable + + attr_reader configuration: Configuration + + def build_client: -> Client + + def configure: () { (Configuration) -> void } -> void + + # Singleton's RBS doesn't quite work + def self.instance: () -> instance + end + end +end diff --git a/sig/openfeature/sdk/client.rbs b/sig/openfeature/sdk/client.rbs new file mode 100644 index 00000000..f44c9983 --- /dev/null +++ b/sig/openfeature/sdk/client.rbs @@ -0,0 +1,17 @@ +module OpenFeature + module SDK + class Client + RESULT_TYPE: Array[Symbol] + SUFFIXES: Array[Symbol] + + @context: evaluationContext + @hooks: Array[hook] + + attr_accessor hooks: Array[hook] + attr_reader metadata: Metadata? + attr_reader provider: untyped + + def initialize: (provider: untyped , client_options: untyped, context: untyped) -> void + end + end +end diff --git a/sig/openfeature/sdk/configuration.rbs b/sig/openfeature/sdk/configuration.rbs new file mode 100644 index 00000000..dc5578c1 --- /dev/null +++ b/sig/openfeature/sdk/configuration.rbs @@ -0,0 +1,19 @@ + +module Concurrent + class Array[T] < ::Array[T] + def initialize: (?::Array[T]) -> void + end +end + +module OpenFeature + module SDK + class Configuration + extend Forwardable + + attr_accessor context: evaluationContext + attr_accessor hooks: [hook] + attr_accessor metadata: Metadata + attr_accessor provider: untyped + end + end +end diff --git a/sig/openfeature/sdk/metadata.rbs b/sig/openfeature/sdk/metadata.rbs new file mode 100644 index 00000000..afe46df6 --- /dev/null +++ b/sig/openfeature/sdk/metadata.rbs @@ -0,0 +1,12 @@ +module OpenFeature + module SDK + class Metadata + attr_reader name: String + attr_reader version: String? + + def initialize: (name: String, ?version: String?) -> void + + def ==: (untyped other) -> untyped + end + end +end diff --git a/sig/openfeature/sdk/provider/no_op_provider.rbs b/sig/openfeature/sdk/provider/no_op_provider.rbs new file mode 100644 index 00000000..91f8fd21 --- /dev/null +++ b/sig/openfeature/sdk/provider/no_op_provider.rbs @@ -0,0 +1,30 @@ +module OpenFeature + module SDK + module Provider + class NoOpProvider + REASON_NO_OP: String + NAME: String + class ResolutionDetails[T] < ::Struct[T] + def initialize : (value: T, reason: String) -> void + + attr_reader value(): T + attr_reader reason(): untyped + attr_reader variant(): untyped + attr_reader error_code(): untyped + attr_reader error_message(): untyped + end + + attr_reader metadata: Metadata + + def fetch_boolean_value: (flag_key: String, default_value: bool, ?evaluation_context: evaluationContext?) -> ResolutionDetails[bool] + + def fetch_string_value: (flag_key: String, default_value: String, ?evaluation_context: evaluationContext?) -> ResolutionDetails[String] + + def fetch_number_value: (flag_key: String, default_value: Numeric, ?evaluation_context: evaluationContext?) -> ResolutionDetails[Numeric] + + def fetch_object_value: (flag_key: String, default_value: Hash[String|Symbol, untyped], ?evaluation_context: evaluationContext?) -> ResolutionDetails[Hash[String|Symbol, untyped]] + def no_op: [X] (X) -> ResolutionDetails[X] + end + end + end +end