fake_florence 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: []