fake_florence 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6d2c5123d928582a6c986f0243198c4c4efecdf0
4
+ data.tar.gz: ec1eb451cc86867f18fc23cb37bc226a570d661d
5
+ SHA512:
6
+ metadata.gz: ca69fa7eb6684c6f72401f27ef2c888c44a1c9560fca534bfcc9b62b747ccba271e6af3726587e0b6c4e6ccb6a216114f3e0a01aed49614a8dc27c2699b5351f
7
+ data.tar.gz: c02fdd8aaebffec882f0a9a368825376355140f3bb4cb8199ecf1ee36978a62d59d7ae772eb952107b1b9f54f98f02b58a37e40d258b42f88e54eb0371c51343
data/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 JP Hastings-Spital
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # Fake Florence
2
+
3
+ A command line application, bundled as a gem, which provides a stub interface for [Determinator](https://github.com/deliveroo/determinator) to read from.
4
+
5
+ ## Installation
6
+
7
+ Install this gem with:
8
+
9
+ ```bash
10
+ $ gem install fake_florence
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ Basic usage of the `flo` command line gem looks like this:
16
+
17
+ ```bash
18
+ $ flo start
19
+ Flo now is running at https://flo.dev
20
+ Use other commands to create or edit Feature flags and Experiments.
21
+ See `flo help` for more information
22
+
23
+ $ flo create my_experiment
24
+ my_experiment created and opened for editing
25
+ ```
26
+
27
+ The default config expects that you have [puma-dev](https://github.com/puma/puma-dev) running, and https://flo.dev pointing to port 35600. You can do this with the command:
28
+
29
+ ```bash
30
+ $ echo 35600 > ~/.puma-dev/flo
31
+ ```
32
+
33
+ Once the Fake Florence server is running, point your instance of determinator at Flo and run `flo announce` to announce all the fake features you currently have specified.
34
+
35
+ ## Planned features
36
+
37
+ - Better logging
38
+ - Logging of requests for features received by the server
39
+ - Tests
40
+
41
+ ## Contributing
42
+
43
+ Bug reports and pull requests are welcome on GitHub at https://github.com/deliveroo/fake_florence. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
44
+
45
+ ## License
46
+
47
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
48
+
49
+ ## Code of Conduct
50
+
51
+ Everyone interacting in the FakeFlorence project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/fake_florence/blob/master/CODE_OF_CONDUCT.md).
data/exe/flo ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.push(File.join(__dir__, '../lib'))
3
+ require 'fake_florence'
4
+
5
+ Process.setproctitle('flo')
6
+ FakeFlorence::Cli.start
@@ -0,0 +1,51 @@
1
+ require 'faraday'
2
+
3
+ module FakeFlorence
4
+ module Announcers
5
+ class Routemaster
6
+ attr_reader :name
7
+
8
+ def initialize(url, name: nil)
9
+ @name = name
10
+ @http = Faraday.new(url: url) do |f|
11
+ f.use Faraday::Response::RaiseError
12
+ f.request :json
13
+ f.adapter Faraday.default_adapter
14
+ end
15
+ end
16
+
17
+ def announce(hash)
18
+ t = Time.now.to_i
19
+
20
+ events = []
21
+ hash.each_pair do |type, features|
22
+ features.each do |feature|
23
+ events.push(
24
+ topic: 'feature',
25
+ type: type,
26
+ url: Config.url_for(feature),
27
+ t: t
28
+ )
29
+ end
30
+ end
31
+
32
+ res = @http.post do |req|
33
+ req.body = events.to_json
34
+ end
35
+
36
+ count = events.size
37
+ counter = "#{count} feature#{count == 1 ? '' : 's'}"
38
+ Config.log.info "#{counter} announced to '#{@name}'."
39
+ true
40
+
41
+ rescue Faraday::ConnectionFailed => e
42
+ Config.log.error e.message
43
+ false
44
+
45
+ rescue Faraday::Error::ClientError => e
46
+ Config.log.warn "Could not announce to #{@name}: HTTP #{e.response[:status]}"
47
+ false
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,15 @@
1
+ module FakeFlorence
2
+ module Announcers
3
+ def self.load(config_array)
4
+ config_array.map do |config|
5
+ case config['type']
6
+ when 'routemaster'
7
+ require 'fake_florence/announcers/routemaster'
8
+ Routemaster.new(config['url'], name: config['name'])
9
+ else
10
+ raise "Unsupported announcer: #{config}"
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,174 @@
1
+ require 'thor'
2
+
3
+ module FakeFlorence
4
+ class Cli < Thor
5
+ include Thor::Actions
6
+
7
+ def self.source_root
8
+ File.join(__dir__, '../../templates')
9
+ end
10
+
11
+ def help
12
+ puts <<~MSG
13
+ Fake Florence v#{VERSION} (#{status_text})
14
+
15
+ This is a command line application which will help you with development of Florence & Determinator based experiments and feature flags.
16
+ Find out more about specific commands with `#{set_color 'flo help [COMMAND]', :blue}`, but you're probably looking for:
17
+
18
+ #{set_color '$ flo start', :blue}
19
+ #{set_color '$ flo create my_experiment', :blue}
20
+
21
+ MSG
22
+ super
23
+ end
24
+
25
+ desc 'start', 'Starts the Florence server'
26
+ long_desc <<~MSG
27
+ Begins a Fake Florence webserver bound to the address and port given in the config file.
28
+ Typical usage is to run `flo start` then use `flo create` and `flo edit` to configure your features.
29
+ MSG
30
+
31
+ method_option(:daemonize,
32
+ aliases: '-d',
33
+ type: :boolean,
34
+ default: true,
35
+ desc: 'Run in the background'
36
+ )
37
+
38
+ def start
39
+ if daemon.running?
40
+ abort 'Flo is already running.'
41
+ else
42
+ puts <<~MSG
43
+ Flo is now #{set_color 'running', :green} at #{set_color Config.base_url, :white}
44
+ Use other commands to create or edit Feature flags and Experiments.
45
+ See `#{set_color 'flo help', :blue}` for more information
46
+ MSG
47
+
48
+ daemon.start(daemonize: options[:daemonize])
49
+ end
50
+ end
51
+
52
+ desc 'stop', 'Stops the Florence server'
53
+ long_desc <<~MSG
54
+ Stops any daemon or foregrounded copy of Fake Florence running on this machine.
55
+ This uses the PID file in the config directory for reference, so its location has been changed versions of Fake Florence may still be running.
56
+ MSG
57
+
58
+ def stop
59
+ if daemon.running?
60
+ daemon.stop
61
+ puts "Flo is now #{set_color 'stopped', :red}."
62
+ else
63
+ abort 'Flo was not running.'
64
+ end
65
+ end
66
+
67
+ desc 'status', 'The status of the Florence server'
68
+ long_desc <<~MSG
69
+ Declares whether Fake Florence is running or not.
70
+ MSG
71
+
72
+ def status
73
+ puts "Flo is #{status_text}"
74
+ end
75
+
76
+ desc 'edit [NAME]', 'Opens Feature Flags and Experiments for editing'
77
+ long_desc <<~MSG
78
+ Uses the editor defined in the '$EDITOR' environment variable to edit a given feature, by name.
79
+ If no name is given, the editor will be passed the folder name.
80
+ MSG
81
+
82
+ def edit(id = nil)
83
+ path = id.nil? ? cache.root : cache.id_to_file(id)
84
+
85
+ unless path.exist?
86
+ puts "This feature doesn't exist. In future, `#{set_color "flo create #{id}", :blue}` will provide a template to start from."
87
+ end
88
+
89
+ open_editor(path)
90
+ puts "#{set_color(id || 'The features folder', :white)} has been opened for editing"
91
+ end
92
+
93
+ desc 'create NAME', 'Creates a new feature for editing'
94
+ long_desc <<~MSG
95
+ Creates a new feature with the given name, with default configuration and opens it for editing.
96
+ MSG
97
+
98
+ def create(id)
99
+ path = cache.id_to_file(id)
100
+ copy_file('feature.yaml', path)
101
+ open_editor(path)
102
+ puts "#{set_color id, :white} created and opened for editing"
103
+ end
104
+
105
+ desc 'clone URL', 'Clones the features on the given server locally'
106
+ long_desc <<~MSG
107
+ Copies all the feature flags and experiments from a given remote server to the local instance of Fake Florence.
108
+ This is particularly useful for making your local instance of Fake Florence look like production.
109
+ MSG
110
+
111
+ def clone(url)
112
+ r = Retriever.new(url)
113
+
114
+ r.each_feature do |feature|
115
+ create_file(cache.id_to_file(feature.id)) do
116
+ feature.saveable
117
+ end
118
+ end
119
+ rescue Retriever::HTTPFailure => e
120
+ abort "HTTP error: #{e.message}"
121
+ rescue Retriever::NotFlorenceServerError
122
+ abort "The given URL doesn't quack like a Florence server"
123
+ end
124
+
125
+ desc 'announce', 'Announces all local features'
126
+
127
+ def announce
128
+ abort "Florence is #{status_text}, please `#{set_colour 'flo start', :blue}` before announcing." unless daemon.running?
129
+ Config.log.level = Logger::WARN
130
+ res = cache.announce_all_now
131
+ announcers = res[:announcers].map do |name|
132
+ set_color name, :white
133
+ end
134
+
135
+ if announcers.any?
136
+ puts "Announced #{set_color res[:features], :white} features to #{announcers.join(', ')}."
137
+ else
138
+ puts 'Did not announce any features.'
139
+ end
140
+ end
141
+
142
+ desc 'config', 'Opens the config file for editing'
143
+
144
+ def config
145
+ open_editor(Config.store_file)
146
+ end
147
+
148
+ desc 'logs', 'Tails the logfile'
149
+
150
+ def logs
151
+ exec("tail -f \"#{Config.logfile}\"")
152
+ end
153
+
154
+ private
155
+
156
+ def open_editor(path)
157
+ abort "Unsure how to edit #{path.to_s}\nEDITOR variable is empty" unless ENV['EDITOR']
158
+ system("$EDITOR \"#{path.to_s}\"")
159
+ abort "Flo failed to open #{path.to_s} for editing" unless $CHILD_STATUS.exitstatus == 0
160
+ end
161
+
162
+ def cache
163
+ @cache ||= FeatureCache.new(Config.home_dir)
164
+ end
165
+
166
+ def daemon
167
+ @daemon ||= FakeFlorence::Daemon.new
168
+ end
169
+
170
+ def status_text
171
+ daemon.running? ? set_color('running', :green) : set_color('stopped', :red)
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,78 @@
1
+ require 'logger'
2
+ require 'pathname'
3
+ require 'uri'
4
+ require 'yaml'
5
+
6
+ module FakeFlorence
7
+ module Config
8
+ class << self
9
+ DEFAULT_CONFIG = Pathname.new(__dir__).join('../../templates/config.yaml').freeze
10
+ SEVERITY = {
11
+ 'DEBUG' => '🤖',
12
+ 'INFO' => 'ℹ️',
13
+ 'WARN' => '⚠️',
14
+ 'ERROR' => '😡',
15
+ 'FATAL' => '🤢',
16
+ }.freeze
17
+
18
+ attr_accessor :log
19
+
20
+ def url_for(feature)
21
+ URI.join(
22
+ read_config(:base_url),
23
+ File.join(
24
+ mount_path,
25
+ feature.id
26
+ )
27
+ )
28
+ end
29
+
30
+ def store_file
31
+ home_dir.join('config.yaml')
32
+ end
33
+
34
+ def home_dir
35
+ Pathname.new('~/.flo').expand_path.tap do |home|
36
+ home.mkpath
37
+ end
38
+ end
39
+
40
+ def log
41
+ @log ||= Logger.new($stdout).tap do |logger|
42
+ logger.formatter = -> (sev, time, name, msg) do
43
+ "#{time.utc.strftime('%Y-%m-%d %H:%M:%SZ')} #{SEVERITY[sev]}: #{msg}\n"
44
+ end
45
+ end
46
+ end
47
+
48
+ def pidfile
49
+ home_dir.join('flo.pid')
50
+ end
51
+
52
+ def logfile
53
+ home_dir.join('flo.log')
54
+ end
55
+
56
+ def method_missing(m)
57
+ read_config(m)
58
+ end
59
+
60
+ private
61
+
62
+ def read_config(key)
63
+ @config_data ||= begin
64
+ if store_file.exist?
65
+ YAML.load_file(store_file)
66
+ else
67
+ YAML.load_file(DEFAULT_CONFIG).tap do |data|
68
+ File.open(store_file, 'w') do |f|
69
+ YAML.dump(data, f)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ @config_data[key.to_s]
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,66 @@
1
+ module FakeFlorence
2
+ class Daemon
3
+ def initialize
4
+ @cache = FeatureCache.new(Config.home_dir)
5
+ @announcers = Announcers.load(Config.announce)
6
+ end
7
+
8
+ def run
9
+ start_listeners
10
+ Server.set :feature_cache, @cache
11
+ Config.log.info "Florence is listening at #{Config.base_url}"
12
+
13
+ prepare_for_run
14
+ Server.run!
15
+ end
16
+
17
+ def start(daemonize:)
18
+ raise 'Already running' if running?
19
+
20
+ if daemonize
21
+ $stdout = Config.logfile.open('a')
22
+ $stdout.sync = true
23
+ Process.daemon(true)
24
+ end
25
+
26
+ run
27
+ end
28
+
29
+ def stop
30
+ raise 'Not running' unless running?
31
+
32
+ Process.kill('TERM', pid)
33
+ end
34
+
35
+ def running?
36
+ return false if pid.nil?
37
+ Process.getpgid(pid)
38
+ true
39
+ rescue Errno::ESRCH
40
+ Config.pidfile.delete
41
+ false
42
+ end
43
+
44
+ def pid
45
+ Config.pidfile.read.to_i
46
+ rescue Errno::ENOENT
47
+ nil
48
+ end
49
+
50
+ private
51
+
52
+ def start_listeners
53
+ @announcers.each do |announcer|
54
+ @cache.register_listener(&announcer.method(:announce))
55
+ end
56
+ end
57
+
58
+ def prepare_for_run
59
+ Config.pidfile.write(Process.pid)
60
+ at_exit do
61
+ Config.pidfile.delete if Config.pidfile.exist?
62
+ Config.log.info 'Shutting down'
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,30 @@
1
+ require 'yaml'
2
+ require 'json'
3
+ require 'ostruct'
4
+
5
+ module FakeFlorence
6
+ class Feature < OpenStruct
7
+ def self.read(id, pathname)
8
+ new(YAML.load_file(pathname)).tap do |feature|
9
+ feature[:id] = id
10
+ end
11
+ end
12
+
13
+ def initialize(keys = {})
14
+ super(FeatureSchema.call(keys).to_h)
15
+ end
16
+
17
+ def to_h
18
+ each_pair.reduce({}) do |h, (k, v)|
19
+ h[k.to_s] = v
20
+ h
21
+ end
22
+ end
23
+
24
+ def saveable
25
+ to_h.tap do |h|
26
+ h.delete('id')
27
+ end.to_yaml
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,75 @@
1
+ require 'listen'
2
+
3
+ module FakeFlorence
4
+ class FeatureCache
5
+ attr_reader :root
6
+
7
+ def initialize(path)
8
+ @root = path.join('features')
9
+ @root.mkpath
10
+ @callbacks = []
11
+ end
12
+
13
+ def features
14
+ Dir.glob(@root.join('*.yaml')).map(&method(:file_to_feature))
15
+ end
16
+
17
+ def feature(id)
18
+ Feature.read(id, id_to_file(id))
19
+ end
20
+
21
+ def register_listener(&block)
22
+ @callbacks.push(block)
23
+ listen_for_changes unless @listener
24
+ end
25
+
26
+ def id_to_file(id)
27
+ @root.join("#{id}.yaml")
28
+ end
29
+
30
+ def announce_all_now
31
+ names = []
32
+
33
+ Announcers.load(Config.announce).each do |announcer|
34
+ names.push(announcer.name) if announcer.announce(noop: features)
35
+ end
36
+
37
+ { features: features.size, announcers: names }
38
+ end
39
+
40
+ private
41
+
42
+ def listen_for_changes
43
+ @listener = Listen.to(@root.to_s, only: /.yaml$/) do |updated, created, deleted|
44
+ features_map = {
45
+ create: created.map(&method(:file_to_feature)),
46
+ update: updated.map(&method(:file_to_feature)),
47
+ delete: deleted.map(&method(:file_to_stub_feature)),
48
+ }
49
+
50
+ features_map.each_pair do |type, fs|
51
+ next if fs.empty?
52
+ Config.log.debug "Features #{type}d: #{fs.map(&:id).join(', ')}"
53
+ end
54
+
55
+ @callbacks.each do |callback|
56
+ callback.call(features_map)
57
+ end
58
+ end
59
+
60
+ @listener.start
61
+ end
62
+
63
+ def file_to_feature(file)
64
+ Feature.read(file_to_id(file), file)
65
+ end
66
+
67
+ def file_to_stub_feature(file)
68
+ Feature.new('id' => file_to_id(file))
69
+ end
70
+
71
+ def file_to_id(file)
72
+ File.basename(file, '.yaml')
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,28 @@
1
+ require 'dry-validation'
2
+
3
+ module FakeFlorence
4
+ FeatureSchema = Dry::Validation.Schema do
5
+ configure { config.input_processor = :sanitizer }
6
+
7
+ required('id').maybe(:str?)
8
+
9
+ required('name').filled(:str?)
10
+ required('identifier').filled(:str?)
11
+ required('bucket_type').filled(:str?)
12
+ required('active').filled(:bool?)
13
+ required('target_groups').each do
14
+ schema do
15
+ required('rollout') { int? & gteq?(0) & lteq?(65536) }
16
+ # TODO: any key => :str?
17
+ required('constraints').filled(:hash?)
18
+ end
19
+ end
20
+
21
+ # TODO: any key => :int?
22
+ required('variants').filled(:hash?)
23
+ required('winning_variant').filled(:str?)
24
+
25
+ # TODO: any key => :str?, :bool?
26
+ required('overrides', Dry::Types::Hash).filled
27
+ end
28
+ end
@@ -0,0 +1,47 @@
1
+ require 'faraday'
2
+ require 'pp'
3
+ require 'faraday_middleware'
4
+
5
+ module FakeFlorence
6
+ class Retriever
7
+
8
+ class NotFlorenceServerError < RuntimeError; end
9
+ class HTTPFailure < RuntimeError; end
10
+
11
+ def initialize(root_url)
12
+ @root = root_url
13
+
14
+ @http = Faraday.new do |f|
15
+ f.response :json, content_type: /\bjson$/
16
+ f.use Faraday::Response::RaiseError
17
+ f.adapter Faraday.default_adapter
18
+ end
19
+ end
20
+
21
+ def each_feature
22
+ urls = [@root]
23
+
24
+ while url = urls.shift
25
+ response = @http.get(url)
26
+ links = response.body['_links']
27
+ raise NotFlorenceServerError if links.nil?
28
+
29
+ urls << links['next']['href'] if links['next']
30
+
31
+ (links['features'] ||[]).each do |listing|
32
+ yield get_feature(listing['href'])
33
+ end
34
+ end
35
+ rescue Faraday::Error::ClientError => e
36
+ Config.log.debug e.message
37
+ raise HTTPFailure
38
+ end
39
+
40
+ private
41
+
42
+ def get_feature(url)
43
+ response = @http.get(url)
44
+ Feature.new(response.body)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,66 @@
1
+ require 'sinatra/base'
2
+
3
+ module FakeFlorence
4
+ class Server < Sinatra::Base
5
+ set :bind, Config.bind
6
+ set :port, Config.port
7
+ set :quiet, false
8
+ set :show_exceptions, false
9
+ set :server, :puma
10
+ set :server_settings, { Silent: true }
11
+ set :environment, :production
12
+
13
+ use Rack::CommonLogger, Config.log
14
+
15
+ helpers do
16
+ def feature
17
+ settings.feature_cache.feature(params[:id])
18
+ rescue Errno::ENOENT
19
+ halt 404
20
+ end
21
+
22
+ def url_for(*path_bits)
23
+ File.join(Config.base_url, *path_bits)
24
+ end
25
+
26
+ def jsonhal_for(*path_bits, extra_links: {})
27
+ {
28
+ _links: {
29
+ curies: [],
30
+ self: { href: url_for(*path_bits) }
31
+ }.merge(extra_links)
32
+ }.tap { |doc|
33
+ doc.merge!(yield) if block_given?
34
+ }.to_json
35
+ end
36
+ end
37
+
38
+ before do
39
+ content_type :json
40
+ end
41
+
42
+ get '/' do
43
+ jsonhal_for('/', extra_links: {
44
+ features: { href: url_for(Config.mount_path) },
45
+ feature: {
46
+ href: url_for(File.join(Config.mount_path, '{id}')),
47
+ templated: true
48
+ }
49
+ })
50
+ end
51
+
52
+ get Config.mount_path do
53
+ jsonhal_for(Config.mount_path, extra_links: {
54
+ features: settings.feature_cache.features.map { |feature|
55
+ { href: url_for(Config.mount_path, feature.id) }
56
+ }
57
+ })
58
+ end
59
+
60
+ get File.join(Config.mount_path, ':id') do
61
+ jsonhal_for(Config.mount_path, feature.id) do
62
+ feature.to_h
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,3 @@
1
+ module FakeFlorence
2
+ VERSION = '1.0.0'
3
+ end
@@ -0,0 +1,14 @@
1
+ require 'fake_florence/announcers'
2
+ require 'fake_florence/cli'
3
+ require 'fake_florence/config'
4
+ require 'fake_florence/daemon'
5
+ require 'fake_florence/feature'
6
+ require 'fake_florence/feature_cache'
7
+ require 'fake_florence/feature_schema'
8
+ require 'fake_florence/retriever'
9
+ require 'fake_florence/server'
10
+ require 'fake_florence/version'
11
+
12
+ module FakeFlorence
13
+
14
+ end
@@ -0,0 +1,18 @@
1
+ ## Server settings
2
+
3
+ # The address the server is going to listen on. Probably best to stick local.
4
+ bind: 127.0.0.1
5
+ # The port that Fake Florence will run on.
6
+ port: 35600
7
+ # This will be the URL Fake Florence uses in its responses. Using puma-dev is
8
+ # recommended (https://github.com/puma/puma-dev)
9
+ base_url: https://flo.dev
10
+ # The path at which feature information will be mouted. It's highly unlikely you
11
+ # need to change this.
12
+ mount_path: /features
13
+ # These are the services that will be pinged whenever a feature is added, edited
14
+ # or removed. This can be an empty array if you like.
15
+ announce:
16
+ - type: routemaster
17
+ name: Determinator Example
18
+ url: https://determinator-example.dev/events
@@ -0,0 +1,58 @@
1
+ ## A Florence template file
2
+
3
+ # A human readable description of the Feature flag or experiment
4
+ name: A descriptive name
5
+ # The identifier is the salt that determines the seed
6
+ # for the random distribution. Two experiments with the
7
+ # same identifier would have the same actors in the same
8
+ # variants.
9
+ identifier: anything
10
+ # Which of an actor's identifying properties will be used
11
+ # to bucket them into rollouts and variants. 'id' and 'guid'
12
+ # are self-evident, 'fallback' uses the actor's id, if
13
+ # available, or uses the actor's guid if not.
14
+ bucket_type: id
15
+ # Inactive features are always off
16
+ active: true
17
+ # Each target group specifies a rollout fraction and a number
18
+ # of constraints that limit the actors to which this rollout
19
+ # will apply.
20
+ target_groups:
21
+ # The rollout fraction is out of 65536
22
+ - rollout: 65536
23
+ # Every constraint must be matched by the actor for the
24
+ # given rollout to apply. This can be an empty hash to mean
25
+ # 'every actor'.
26
+ constraints:
27
+ # Here, the actor's 'platform' must be 'ios'
28
+ platform: ios
29
+ # Here, the actor's 'country' must be either 'uk' or 'de'
30
+ country: [uk, de]
31
+
32
+ ## Experiment only options
33
+
34
+ # Listing variants declares this an experiment
35
+ # Each key of this hash is the string which will
36
+ # be given if the actor is in that variant.
37
+ # Each value is the weighting of that variant,
38
+ # eg. if all are '1' then the actors have an equal
39
+ # chance of being in each variant.
40
+ variants:
41
+ # The first is always considered the control.
42
+ anchovy: 1
43
+ mackerel: 1
44
+ # A non-null value declares the winner of the experiment
45
+ # and the result that all actors within any target group's
46
+ # rollout will see.
47
+ winning_variant: anchovy
48
+
49
+ # Overrides force specific actors to have a given outcome,
50
+ # instead of the determinated one. Because actors must be
51
+ # specified by identifier this should be kept to a select
52
+ # few for testing and evaluation. Target Groups should be
53
+ # used instead product cases.
54
+ overrides:
55
+ # The key is the actor identifier (ie. the ID or anonymous
56
+ # ID as specified by the buckt type).
57
+ # The key is the desired outcome for this actor.
58
+ 123: false
metadata ADDED
@@ -0,0 +1,190 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fake_florence
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - JP Hastings-Spital
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-09-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: dry-validation
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.11'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.11'
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.13'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.13'
41
+ - !ruby/object:Gem::Dependency
42
+ name: faraday_middleware
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.12'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.12'
55
+ - !ruby/object:Gem::Dependency
56
+ name: listen
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: puma
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.10'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.10'
83
+ - !ruby/object:Gem::Dependency
84
+ name: sinatra
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '2.0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '2.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: thor
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.20'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.20'
111
+ - !ruby/object:Gem::Dependency
112
+ name: bundler
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '1.15'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '1.15'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rake
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '10.0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '10.0'
139
+ description: A local Florence server for use with Determinator locally
140
+ email:
141
+ - jp@deliveroo.co.uk
142
+ executables:
143
+ - flo
144
+ extensions: []
145
+ extra_rdoc_files: []
146
+ files:
147
+ - LICENSE.md
148
+ - README.md
149
+ - exe/flo
150
+ - lib/fake_florence.rb
151
+ - lib/fake_florence/announcers.rb
152
+ - lib/fake_florence/announcers/routemaster.rb
153
+ - lib/fake_florence/cli.rb
154
+ - lib/fake_florence/config.rb
155
+ - lib/fake_florence/daemon.rb
156
+ - lib/fake_florence/feature.rb
157
+ - lib/fake_florence/feature_cache.rb
158
+ - lib/fake_florence/feature_schema.rb
159
+ - lib/fake_florence/retriever.rb
160
+ - lib/fake_florence/server.rb
161
+ - lib/fake_florence/version.rb
162
+ - templates/config.yaml
163
+ - templates/feature.yaml
164
+ homepage: https://github.com/deliveroo/fake_florence
165
+ licenses:
166
+ - MIT
167
+ metadata: {}
168
+ post_install_message: 'Fake Florence has been installed. Run `flo help` for more information.
169
+
170
+ '
171
+ rdoc_options: []
172
+ require_paths:
173
+ - lib
174
+ required_ruby_version: !ruby/object:Gem::Requirement
175
+ requirements:
176
+ - - ">="
177
+ - !ruby/object:Gem::Version
178
+ version: '0'
179
+ required_rubygems_version: !ruby/object:Gem::Requirement
180
+ requirements:
181
+ - - ">="
182
+ - !ruby/object:Gem::Version
183
+ version: '0'
184
+ requirements: []
185
+ rubyforge_project:
186
+ rubygems_version: 2.5.1
187
+ signing_key:
188
+ specification_version: 4
189
+ summary: A local Florence server for use with Determinator locally
190
+ test_files: []