diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index e962a28d..7d27c7ce 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -18,6 +18,10 @@ jobs: bundler-cache: true - name: Run RuboCop run: bundle exec rubocop + - name: Run Steep / RBS Type signature verification + run: | + bundle exec steep check --steepfile=./Steepfile --severity-level=error | ruby -pe 'sub(/^(.+):(\d+):(\d+): (.+)$/, %q{::error file=\1,line=\2,col=\3::\4})' + shell: bash test: runs-on: ${{ matrix.os }}-latest diff --git a/Steepfile b/Steepfile new file mode 100644 index 00000000..88ceffb9 --- /dev/null +++ b/Steepfile @@ -0,0 +1,35 @@ +D = Steep::Diagnostic + +target :lib do + signature "*.rbs" + + # NOTE: All client-exposed methods/types should now have type signatures / rbs support + check "lib/unleash/client.rb" + check "lib/unleash/context.rb" + check "lib/unleash/variant.rb" + check "lib/unleash/scheduled_executor.rb" + + # TODO: add signatures to the rest + # Mostly internal SDK files. + ignore "lib/unleash/bootstrap" + ignore "lib/unleash/strategy/*.rb" + ignore "lib/unleash/util" + + ignore "lib/unleash/constraint.rb" + ignore "lib/unleash/feature_toggle.rb" + ignore "lib/unleash/metrics.rb" + ignore "lib/unleash/metrics_reporter.rb" + ignore "lib/unleash/toggle_fetcher.rb" + ignore "lib/unleash/variant_definition.rb" + ignore "lib/unleash/variant_override.rb" + + # library "pathname", "set" # Standard libraries + # library "strong_json" # Gems + + # configure_code_diagnostics(D::Ruby.strict) # `strict` diagnostics setting + # configure_code_diagnostics(D::Ruby.lenient) # `lenient` diagnostics setting + configure_code_diagnostics do |hash| # You can setup everything yourself + hash[D::Ruby::NoMethod] = :information + hash[D::Ruby::UnsupportedSyntax] = :information + end +end diff --git a/lib/unleash/client.rb b/lib/unleash/client.rb index c88024fe..2abbbacd 100644 --- a/lib/unleash/client.rb +++ b/lib/unleash/client.rb @@ -162,7 +162,7 @@ def register end def disabled_variant - @disabled_variant ||= Unleash::FeatureToggle.disabled_variant + Unleash::FeatureToggle.disabled_variant end def first_fetch_is_eager diff --git a/lib/unleash/client.rbs b/lib/unleash/client.rbs new file mode 100644 index 00000000..b3e17d9f --- /dev/null +++ b/lib/unleash/client.rbs @@ -0,0 +1,39 @@ +module Unleash + class Client + attr_accessor fetcher_scheduled_executor: ScheduledExecutor + + attr_accessor metrics_scheduled_executor: ScheduledExecutor + + def initialize: (*untyped opts) -> void + + def is_enabled?: (untyped feature, ?Context? context, ?bool default_value_param) ?{ () -> bool } -> bool + + # enabled? is a more ruby idiomatic method name than is_enabled? + alias enabled? is_enabled? + + # execute a code block (passed as a parameter), if is_enabled? is true. + def if_enabled: (untyped feature, ?Context context, ?bool default_value) { (untyped) -> bool } -> (untyped | nil) + + def get_variant: (untyped feature, ?Context context, ?Variant fallback_variant) -> Variant + + # safe shutdown: also flush metrics to server and toggles to disk + def shutdown: () -> nil + + # quick shutdown: just kill running threads + def shutdown!: () -> nil + + private + + def info: () -> { appName: String, instanceId: String, sdkVersion: String, strategies: Array[String], started: String, interval: Integer } + + def start_toggle_fetcher: () -> nil + + def start_metrics: () -> nil + + def register: () -> nil + + def disabled_variant: () -> Variant + + def first_fetch_is_eager: () -> bool + end +end diff --git a/lib/unleash/context.rbs b/lib/unleash/context.rbs new file mode 100644 index 00000000..3c544d73 --- /dev/null +++ b/lib/unleash/context.rbs @@ -0,0 +1,21 @@ +module Unleash + class Context + ATTRS: ::Array[:app_name | :environment | :user_id | :session_id | :remote_address | :current_time] + + def initialize: (?::Hash[String, any] params) -> Context + + def to_s: () -> ::String + + def get_by_name: ((String | Symbol) name) -> any + + def include?: ((String | Symbol) name) -> bool + + private + + # Method to fetch values from hash for two types of keys: string in camelCase and symbol in snake_case + def value_for: ((String | Symbol) key, Hash[String, any] params, ?any? default_value) -> any + + # converts CamelCase to snake_case + def underscore: ((String | Symbol) camel_cased_word) -> String + end +end diff --git a/lib/unleash/scheduled_executor.rbs b/lib/unleash/scheduled_executor.rbs new file mode 100644 index 00000000..6751642f --- /dev/null +++ b/lib/unleash/scheduled_executor.rbs @@ -0,0 +1,29 @@ +module Unleash + class ScheduledExecutor + attr_accessor name: String + + attr_accessor interval: Numeric + + attr_accessor max_exceptions: ::Integer + + attr_accessor retry_count: ::Integer + + attr_accessor thread: (Thread | nil) + + attr_accessor immediate_execution: bool + + def initialize: (String name, untyped interval, ?::Integer max_exceptions, ?bool immediate_execution) -> void + + def run: () ?{ () -> nil } -> nil + + def running?: () -> bool + + def exit: () -> nil + + private + + def run_blk: () { () -> nil } -> nil + + def exceeded_max_exceptions?: () -> bool + end +end diff --git a/lib/unleash/variant.rbs b/lib/unleash/variant.rbs new file mode 100644 index 00000000..43486f0b --- /dev/null +++ b/lib/unleash/variant.rbs @@ -0,0 +1,15 @@ +module Unleash + class Variant + attr_accessor name: String + + attr_accessor enabled: bool + + attr_accessor payload: String + + def initialize: (?::Hash[String, String] params) -> void + + def to_s: () -> ::String + + def ==: (Variant other) -> bool + end +end diff --git a/unleash-client.gemspec b/unleash-client.gemspec index ec156341..3b53a9b5 100644 --- a/unleash-client.gemspec +++ b/unleash-client.gemspec @@ -39,4 +39,12 @@ Gem::Specification.new do |spec| spec.add_development_dependency "simplecov", "~> 0.21.2" spec.add_development_dependency "simplecov-lcov", "~> 0.8.0" + + # NOTE: only require rbs/steep in supported ruby versions. In EOL ruby/jruby, just ignore. + # rubocop:disable Gemspec/RubyVersionGlobalsUsage + if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.6') && RUBY_ENGINE != 'jruby' + spec.add_development_dependency "rbs", "~> 2.8" + spec.add_development_dependency "steep", "~> 1.3" + end + # rubocop:enable Gemspec/RubyVersionGlobalsUsage end