Skip to content

Commit

Permalink
Merge pull request #203 from pedro/create-configurable-module
Browse files Browse the repository at this point in the history
Add `Configuration` API
  • Loading branch information
Emanuele Palazzetti authored Oct 19, 2017
2 parents df52c6d + 57a9807 commit 810de9a
Show file tree
Hide file tree
Showing 10 changed files with 416 additions and 0 deletions.
13 changes: 13 additions & 0 deletions lib/ddtrace.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require 'ddtrace/tracer'
require 'ddtrace/error'
require 'ddtrace/pipeline'
require 'ddtrace/configuration'

# \Datadog global namespace that includes all tracing functionality for Tracer and Span classes.
module Datadog
Expand Down Expand Up @@ -30,6 +31,18 @@ def self.tracer
def self.registry
@registry
end

class << self
attr_writer :configuration

def configuration
@configuration ||= Configuration.new
end

def configure
yield(configuration)
end
end
end

# Monkey currently is responsible for loading all contributions, which in turn
Expand Down
77 changes: 77 additions & 0 deletions lib/ddtrace/configurable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
module Datadog
InvalidOptionError = Class.new(StandardError)
# Configurable provides configuration methods for a given class/module
module Configurable
IDENTITY = ->(x) { x }

def self.included(base)
base.singleton_class.send(:include, ClassMethods)
end

# ClassMethods
module ClassMethods
def set_option(name, value)
__assert_valid!(name)

__options[name][:value] = __options[name][:setter].call(value)
__options[name][:set_flag] = true
end

def get_option(name)
__assert_valid!(name)

return __options[name][:default] unless __options[name][:set_flag]

__options[name][:value]
end

def to_h
__options.each_with_object({}) do |(key, _), hash|
hash[key] = get_option(key)
end
end

def reset_options!
__options.each do |name, meta|
set_option(name, meta[:default])
end
end

def sorted_options
Configuration::Resolver.new(__dependency_graph).call
end

private

def option(name, meta = {}, &block)
name = name.to_sym
meta[:setter] ||= (block || IDENTITY)
meta[:depends_on] ||= []
__options[name] = meta
end

def __options
@__options ||= {}
end

def __assert_valid!(name)
return if __options.key?(name)
raise(InvalidOptionError, "#{__pretty_name} doesn't have the option: #{name}")
end

def __pretty_name
entry = Datadog.registry.find { |el| el.klass == self }

return entry.name if entry

to_s
end

def __dependency_graph
__options.each_with_object({}) do |(name, meta), graph|
graph[name] = meta[:depends_on]
end
end
end
end
end
35 changes: 35 additions & 0 deletions lib/ddtrace/configuration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
require_relative 'configuration/proxy'
require_relative 'configuration/resolver'

module Datadog
# Configuration provides a unique access point for configurations
class Configuration
InvalidIntegrationError = Class.new(StandardError)

def initialize(options = {})
@registry = options.fetch(:registry, Datadog.registry)
end

def [](integration_name)
integration = fetch_integration(integration_name)
Proxy.new(integration)
end

def use(integration_name, options = {})
integration = fetch_integration(integration_name)

integration.sorted_options.each do |name|
integration.set_option(name, options[name]) if options.key?(name)
end

integration.patch if integration.respond_to?(:patch)
end

private

def fetch_integration(name)
@registry[name] ||
raise(InvalidIntegrationError, "'#{name}' is not a valid integration.")
end
end
end
29 changes: 29 additions & 0 deletions lib/ddtrace/configuration/proxy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
require 'forwardable'

module Datadog
class Configuration
# Proxy provides a hash-like interface for fetching/setting configurations
class Proxy
extend Forwardable

def initialize(integration)
@integration = integration
end

def [](param)
value = @integration.get_option(param)

return value.call if value.respond_to?(:call)

value
end

def []=(param, value)
@integration.set_option(param, value)
end

def_delegators :@integration, :to_h, :reset_options!
def_delegators :to_h, :to_hash, :merge
end
end
end
24 changes: 24 additions & 0 deletions lib/ddtrace/configuration/resolver.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
require 'tsort'

module Datadog
class Configuration
# Resolver performs a topological sort over the dependency graph
class Resolver
include TSort

def initialize(dependency_graph = {})
@dependency_graph = dependency_graph
end

def tsort_each_node(&blk)
@dependency_graph.each_key(&blk)
end

def tsort_each_child(node, &blk)
@dependency_graph.fetch(node).each(&blk)
end

alias call tsort
end
end
end
2 changes: 2 additions & 0 deletions lib/ddtrace/contrib/base.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
require 'ddtrace/registry'
require 'ddtrace/configurable'

module Datadog
module Contrib
# Base provides features that are shared across all integrations
module Base
def self.included(base)
base.send(:include, Registry::Registerable)
base.send(:include, Configurable)
end
end
end
Expand Down
88 changes: 88 additions & 0 deletions test/configurable_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
require 'ddtrace/configurable'

module Datadog
class ConfigurableTest < Minitest::Test
def setup
@module = Module.new { include(Configurable) }
end

def test_option_methods
assert_respond_to(@module, :set_option)
assert_respond_to(@module, :get_option)
end

def test_option_default
@module.class_eval do
option :foo, default: :bar
end

assert_equal(:bar, @module.get_option(:foo))
end

def test_setting_an_option
@module.class_eval do
option :foo, default: :bar
end

@module.set_option(:foo, 'baz!')
assert_equal('baz!', @module.get_option(:foo))
end

def test_custom_setter
@module.class_eval do
option :shout, setter: ->(v) { v.upcase }
end

@module.set_option(:shout, 'loud')
assert_equal('LOUD', @module.get_option(:shout))
end

def test_custom_setter_block
@module.class_eval do
option(:shout) { |value| "#{value.upcase}!" }
end

@module.set_option(:shout, 'ouch')
assert_equal('OUCH!', @module.get_option(:shout))
end

def test_invalid_option
assert_raises(InvalidOptionError) do
@module.set_option(:bad_option, 'foo')
end

assert_raises(InvalidOptionError) do
@module.get_option(:bad_option)
end
end

def test_to_h
@module.class_eval do
option :x, default: 1
option :y, default: 2
end

@module.set_option(:y, 100)
assert_equal({ x: 1, y: 100 }, @module.to_h)
end

def test_false_options
@module.class_eval do
option :boolean, default: true
end

@module.set_option(:boolean, false)
refute(@module.get_option(:boolean))
end

def test_dependency_solving
@module.class_eval do
option :foo, depends_on: [:bar]
option :bar, depends_on: [:baz]
option :baz
end

assert_equal([:baz, :bar, :foo], @module.sorted_options)
end
end
end
34 changes: 34 additions & 0 deletions test/configuration/proxy_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
require 'ddtrace/configurable'

module Datadog
class Configuration
class ProxyTest < Minitest::Test
def setup
@module = Module.new do
include Configurable
option :x, default: :a
option :y, default: :b
end

@proxy = Proxy.new(@module)
end

def test_hash_syntax
@proxy[:x] = 1
@proxy[:y] = 2

assert_equal(1, @proxy[:x])
assert_equal(2, @proxy[:y])
end

def test_hash_coercion
assert_equal({ x: :a, y: :b }, @proxy.to_h)
assert_equal({ x: :a, y: :b }, @proxy.to_hash)
end

def test_merge
assert_equal({ x: :a, y: :b, z: :c }, @proxy.merge(z: :c))
end
end
end
end
22 changes: 22 additions & 0 deletions test/configuration/resolver_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
require 'ddtrace/configuration'

module Datadog
class Configuration
class ResolverTest < Minitest::Test
def test_dependency_solving
graph = { 1 => [2], 2 => [3, 4], 3 => [], 4 => [3], 5 => [1] }
tsort = Resolver.new(graph).call

assert_equal([3, 4, 2, 1, 5], tsort)
end

def test_cyclic_dependecy
graph = { 1 => [2], 2 => [1] }

assert_raises(TSort::Cyclic) do
Resolver.new(graph).call
end
end
end
end
end
Loading

0 comments on commit 810de9a

Please sign in to comment.