Skip to content

Commit

Permalink
feat: make bootstrapping more ruby idiomatic
Browse files Browse the repository at this point in the history
  • Loading branch information
rarruda committed Jan 29, 2022
1 parent 5cc8b9a commit 092b8b4
Show file tree
Hide file tree
Showing 11 changed files with 124 additions and 102 deletions.
11 changes: 8 additions & 3 deletions README.md
Expand Up @@ -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`.

Expand All @@ -95,7 +95,7 @@ require 'unleash/bootstrap'
app_name: 'my_ruby_app',
url: 'http://unleash.herokuapp.com/api',
custom_http_headers: { 'Authorization': '<API token>' },
bootstrapper: Unleash::FileBootStrapper.new('./default-toggles.json')
bootstrapper: Unleash::Bootstrap::FromFile.new('./default-toggles.json')
)

feature_name = "AwesomeFeature"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
5 changes: 2 additions & 3 deletions examples/bootstrap.rb
Expand Up @@ -2,7 +2,7 @@

require 'unleash'
require 'unleash/context'
require 'unleash/bootstrap'
require 'unleash/bootstrap/from_file'

puts ">> START bootstrap.rb"

Expand All @@ -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
Expand Down
55 changes: 0 additions & 55 deletions lib/unleash/bootstrap.rb

This file was deleted.

18 changes: 18 additions & 0 deletions 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
18 changes: 18 additions & 0 deletions 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
20 changes: 20 additions & 0 deletions 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
7 changes: 4 additions & 3 deletions lib/unleash/toggle_fetcher.rb
Expand Up @@ -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."
Expand Down
9 changes: 6 additions & 3 deletions lib/unleash/util/http.rb
Expand Up @@ -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
Expand All @@ -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?

Expand Down
22 changes: 22 additions & 0 deletions 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
26 changes: 26 additions & 0 deletions 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
35 changes: 0 additions & 35 deletions spec/unleash/bootstrap_spec.rb

This file was deleted.

0 comments on commit 092b8b4

Please sign in to comment.