diff --git a/README.md b/README.md index 017c280d..7dfff2b4 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ Argument | Description | Required? | Type | Default Value| `backup_file` | Filename to store the last known state from the Unleash server. Best to not change this from the default. | N | String | `Dir.tmpdir + "/unleash-#{app_name}-repo.json` | `logger` | Specify a custom `Logger` class to handle logs for the Unleash client. | N | Class | `Logger.new(STDOUT)` | `log_level` | Change the log level for the `Logger` class. Constant from `Logger::Severity`. | N | Constant | `Logger::WARN` | -`bootstrapper` | Defines a bootstrapper object that unleash will use to get a list of toggles on load before it reads them from the unleash server. This is useful for loading large states on startup without hitting the network. Bootstrapping classes are provided for URL and file reading but you can implement your own for other sources of toggles. | N | Class | `nil` | +`bootstrapper` | Bootstrapper object to get a list of toggles on load before it reads them from the unleash server. This is useful for loading large states on startup without hitting the network. Bootstrapping classes are provided for URL and file reading but you can implement your own for other sources of toggles. | N | Unleash::Bootstrap::Base or Nil | `nil` | For in a more in depth look, please see `lib/unleash/configuration.rb`. @@ -95,7 +95,7 @@ require 'unleash/bootstrap' app_name: 'my_ruby_app', url: 'http://unleash.herokuapp.com/api', custom_http_headers: { 'Authorization': '' }, - bootstrapper: Unleash::FileBootStrapper.new('./default-toggles.json') + bootstrapper: Unleash::Bootstrap::FromFile.new('./default-toggles.json') ) feature_name = "AwesomeFeature" @@ -124,7 +124,6 @@ Unleash.configure do |config| # config.instance_id = "#{Socket.gethostname}" config.logger = Rails.logger config.environment = Rails.env - config.bootstrapper = Unleash::FileBootStrapper.new('./default-toggles.json') end UNLEASH = Unleash::Client.new @@ -315,6 +314,12 @@ This client comes with the all the required strategies out of the box: * UnknownStrategy * UserWithIdStrategy +## Available Bootstrap Classes + +This client comes with these classes to load unleash features on startup, before making a request to the Unleash API: + + * Unleash::Bootstrap::FromFile + * Unleash::Bootstrap::FromUri ## Development diff --git a/examples/bootstrap.rb b/examples/bootstrap.rb index 64379571..75427e1c 100755 --- a/examples/bootstrap.rb +++ b/examples/bootstrap.rb @@ -2,7 +2,7 @@ require 'unleash' require 'unleash/context' -require 'unleash/bootstrap' +require 'unleash/bootstrap/from_file' puts ">> START bootstrap.rb" @@ -14,10 +14,9 @@ refresh_interval: 2, metrics_interval: 2, retry_limit: 2, - bootstrapper: Unleash::FileBootStrapper.new('./examples/default-toggles.json') + bootstrapper: Unleash::Bootstrap::FromFile.new('./examples/default-toggles.json') ) -# feature_name = "AwesomeFeature" feature_name = "featureX" unleash_context = Unleash::Context.new unleash_context.user_id = 123 diff --git a/lib/unleash/bootstrap.rb b/lib/unleash/bootstrap.rb deleted file mode 100644 index 2e37c03e..00000000 --- a/lib/unleash/bootstrap.rb +++ /dev/null @@ -1,55 +0,0 @@ -require 'unleash/configuration' -require 'unleash/feature_toggle' -require 'logger' -require 'time' -require 'net/http' -require 'uri' - -module Unleash - class FileBootStrapper - attr_accessor :file_path - - def initialize(file_path) - self.file_path = file_path - end - - def read - file_content = File.read(self.file_path) - bootstrap_hash = JSON.parse(file_content) - Unleash.extract_bootstrap(bootstrap_hash) - end - end - - class UrlBootStrapper - attr_accessor :uri, :headers - - def initialize(uri, headers) - self.uri = URI(uri) - self.headers = headers - end - - def read - request = Net::HTTP::Get.new(self.uri, self.build_headers(self.headers)) - - http = Net::HTTP.new(uri.host, uri.port) - http.use_ssl = true if uri.scheme == 'https' - http.open_timeout = Unleash.configuration.timeout - http.read_timeout = Unleash.configuration.timeout - - http.request(request) - end - - def build_headers(headers = nil) - headers = (headers || {}).dup - headers['Content-Type'] = 'application/json' - - headers - end - end - - def self.extract_bootstrap(bootstrap_hash) - raise NotImplemented, "The provided bootstrap doesn't seem to be a valid set of toggles" if bootstrap_hash['version'] < 1 - - bootstrap_hash['features'] - end -end diff --git a/lib/unleash/bootstrap/base.rb b/lib/unleash/bootstrap/base.rb new file mode 100644 index 00000000..ceb090b5 --- /dev/null +++ b/lib/unleash/bootstrap/base.rb @@ -0,0 +1,18 @@ +module Unleash + module Bootstrap + class NotImplemented < RuntimeError + end + + class Base + def read + raise NotImplemented, "Bootstrap is not implemented" + end + + def extract_features(bootstrap_hash) + raise NotImplemented, "The provided bootstrap data doesn't seem to have a valid set of toggles" if bootstrap_hash['version'] < 1 + + bootstrap_hash['features'] + end + end + end +end diff --git a/lib/unleash/bootstrap/from_file.rb b/lib/unleash/bootstrap/from_file.rb new file mode 100644 index 00000000..2995a188 --- /dev/null +++ b/lib/unleash/bootstrap/from_file.rb @@ -0,0 +1,18 @@ +module Unleash + module Bootstrap + class FromFile < Base + attr_accessor :file_path + + # @param file_path [String] + def initialize(file_path) + self.file_path = file_path + end + + def read + file_content = File.read(self.file_path) + bootstrap_hash = JSON.parse(file_content) + extract_features(bootstrap_hash) + end + end + end +end diff --git a/lib/unleash/bootstrap/from_uri.rb b/lib/unleash/bootstrap/from_uri.rb new file mode 100644 index 00000000..64f58aaa --- /dev/null +++ b/lib/unleash/bootstrap/from_uri.rb @@ -0,0 +1,20 @@ +module Unleash + module Bootstrap + class FromUri < Base + attr_accessor :uri, :headers + + # @param uri [String] + # @param headers [Hash, nil] HTTP headers to use. If not set, the unleash client SDK ones will be used. + def initialize(uri, headers = nil) + self.uri = URI(uri) + self.headers = headers + end + + def read + response = Unleash::Util::Http.get(self.uri, nil, self.headers) + bootstrap_hash = JSON.parse(response.body) + extract_features(bootstrap_hash) + end + end + end +end diff --git a/lib/unleash/toggle_fetcher.rb b/lib/unleash/toggle_fetcher.rb index beb0bf03..558061d8 100755 --- a/lib/unleash/toggle_fetcher.rb +++ b/lib/unleash/toggle_fetcher.rb @@ -16,11 +16,12 @@ def initialize # start by fetching synchronously, and failing back to reading the backup file. begin - if !self.bootstrapper.nil? # if the consumer provides a bootstrapper, we're going to assume they want to use it + if self.bootstrapper.nil? + fetch + else + # if the consumer provides a bootstrapper, use it! synchronize_with_local_cache! self.bootstrapper.read update_running_client! - else - fetch end rescue StandardError => e Unleash.logger.warn "ToggleFetcher was unable to fetch from the network or bootstrap, attempting to read from backup file." diff --git a/lib/unleash/util/http.rb b/lib/unleash/util/http.rb index 7b4d19ea..57c2e34d 100644 --- a/lib/unleash/util/http.rb +++ b/lib/unleash/util/http.rb @@ -4,10 +4,10 @@ module Unleash module Util module Http - def self.get(uri, etag = nil) + def self.get(uri, etag = nil, headers_override = nil) http = http_connection(uri) - request = Net::HTTP::Get.new(uri.request_uri, http_headers(etag)) + request = Net::HTTP::Get.new(uri.request_uri, http_headers(etag, headers_override)) http.request(request) end @@ -30,10 +30,13 @@ def self.http_connection(uri) http end - def self.http_headers(etag = nil) + # @param etag [String, nil] + # @param headers_override [Hash, nil] + def self.http_headers(etag = nil, headers_override = nil) Unleash.logger.debug "ETag: #{etag}" unless etag.nil? headers = (Unleash.configuration.http_headers || {}).dup + headers = headers_override if headers_override.is_a?(Hash) headers['Content-Type'] = 'application/json' headers['If-None-Match'] = etag unless etag.nil? diff --git a/spec/unleash/bootstrap/from_file_spec.rb b/spec/unleash/bootstrap/from_file_spec.rb new file mode 100644 index 00000000..2d56b20b --- /dev/null +++ b/spec/unleash/bootstrap/from_file_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' +require 'rspec/json_expectations' +require 'unleash/bootstrap/base' +require 'unleash/bootstrap/from_file' +require 'json' + +RSpec.describe Unleash::Bootstrap::FromFile do + before do + Unleash.configuration = Unleash::Configuration.new + Unleash.logger = Unleash.configuration.logger + end + + it 'loads bootstrap toggle correctly from file' do + bootstrap_file = './spec/unleash/bootstrap-resources/features-v1.json' + + bootstrapper = Unleash::Bootstrap::FromFile.new(bootstrap_file) + file_contents = File.open(bootstrap_file).read + file_features = JSON.parse(file_contents)['features'] + + expect(bootstrapper.read).to include_json(file_features) + end +end diff --git a/spec/unleash/bootstrap/from_uri_spec.rb b/spec/unleash/bootstrap/from_uri_spec.rb new file mode 100644 index 00000000..ef2edd09 --- /dev/null +++ b/spec/unleash/bootstrap/from_uri_spec.rb @@ -0,0 +1,26 @@ +require 'unleash/bootstrap/from_uri' +require 'json' + +RSpec.describe Unleash::Bootstrap::FromUri do + it 'loads bootstrap toggle correctly from URL' do + bootstrap_file = './spec/unleash/bootstrap-resources/features-v1.json' + + file_contents = File.open(bootstrap_file).read + file_features = JSON.parse(file_contents)['features'] + + WebMock.stub_request(:get, "http://test-url/bootstrap-goodness") + .with( + headers: { + 'Accept' => '*/*', + 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', + 'Content-Type' => 'application/json', + 'User-Agent' => 'Ruby' + } + ) + .to_return(status: 200, body: file_contents, headers: {}) + + bootstrapper = Unleash::Bootstrap::FromUri.new('http://test-url/bootstrap-goodness', {}) + + expect(bootstrapper.read).to include_json(file_features) + end +end diff --git a/spec/unleash/bootstrap_spec.rb b/spec/unleash/bootstrap_spec.rb deleted file mode 100644 index 9c871786..00000000 --- a/spec/unleash/bootstrap_spec.rb +++ /dev/null @@ -1,35 +0,0 @@ -require 'spec_helper' -require 'unleash/constraint' -require 'unleash/bootstrap' -require 'json' - -RSpec.describe Unleash::Client do - before do - Unleash.configuration = Unleash::Configuration.new - Unleash.logger = Unleash.configuration.logger - end - - describe 'Bootstrap' do - it 'loads bootstrap toggle correctly from file' do - bootstrapper = Unleash::FileBootStrapper.new('./spec/unleash/bootstrap-resources/features-v1.json') - bootstrapper.read - end - - it 'loads bootstrap toggle correctly from URL' do - WebMock.stub_request(:get, "http://test-url/bootstrap-goodness") - .with( - headers: { - 'Accept' => '*/*', - 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', - 'Content-Type' => 'application/json', - 'Host' => 'test-url', - 'User-Agent' => 'Ruby' - } - ) - .to_return(status: 200, body: "", headers: {}) - - bootstrapper = Unleash::UrlBootStrapper.new('http://test-url/bootstrap-goodness', nil) - bootstrapper.read - end - end -end