circuit 0.2.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.
Files changed (75) hide show
  1. data/Gemfile +34 -0
  2. data/LICENSE +20 -0
  3. data/README.md +161 -0
  4. data/Rakefile +27 -0
  5. data/config.ru +7 -0
  6. data/description.md +5 -0
  7. data/docs/COMPATIBILITY.md +14 -0
  8. data/docs/ROADMAP.md +29 -0
  9. data/lib/circuit.rb +125 -0
  10. data/lib/circuit/behavior.rb +99 -0
  11. data/lib/circuit/compatibility.rb +73 -0
  12. data/lib/circuit/middleware.rb +6 -0
  13. data/lib/circuit/middleware/rewriter.rb +43 -0
  14. data/lib/circuit/rack.rb +14 -0
  15. data/lib/circuit/rack/behavioral.rb +45 -0
  16. data/lib/circuit/rack/builder.rb +50 -0
  17. data/lib/circuit/rack/multi_site.rb +22 -0
  18. data/lib/circuit/rack/request.rb +81 -0
  19. data/lib/circuit/railtie.rb +24 -0
  20. data/lib/circuit/storage.rb +74 -0
  21. data/lib/circuit/storage/memory_model.rb +70 -0
  22. data/lib/circuit/storage/nodes.rb +56 -0
  23. data/lib/circuit/storage/nodes/memory_store.rb +63 -0
  24. data/lib/circuit/storage/nodes/model.rb +67 -0
  25. data/lib/circuit/storage/nodes/mongoid_store.rb +56 -0
  26. data/lib/circuit/storage/sites.rb +38 -0
  27. data/lib/circuit/storage/sites/memory_store.rb +53 -0
  28. data/lib/circuit/storage/sites/model.rb +29 -0
  29. data/lib/circuit/storage/sites/mongoid_store.rb +43 -0
  30. data/lib/circuit/validators.rb +74 -0
  31. data/lib/circuit/version.rb +3 -0
  32. data/spec/internal/app/behaviors/change_path.rb +7 -0
  33. data/spec/internal/app/controllers/application_controller.rb +5 -0
  34. data/spec/internal/app/helpers/application_helper.rb +2 -0
  35. data/spec/internal/config/initializers/circuit.rb +7 -0
  36. data/spec/internal/config/routes.rb +3 -0
  37. data/spec/internal/db/schema.rb +1 -0
  38. data/spec/lib/circuit/behavior_spec.rb +113 -0
  39. data/spec/lib/circuit/middleware/rewriter_spec.rb +79 -0
  40. data/spec/lib/circuit/rack/behavioral_spec.rb +60 -0
  41. data/spec/lib/circuit/rack/builder_spec.rb +125 -0
  42. data/spec/lib/circuit/rack/multi_site_spec.rb +34 -0
  43. data/spec/lib/circuit/rack/request_spec.rb +80 -0
  44. data/spec/lib/circuit/railtie_spec.rb +34 -0
  45. data/spec/lib/circuit/storage/nodes_spec.rb +62 -0
  46. data/spec/lib/circuit/storage/sites_spec.rb +60 -0
  47. data/spec/lib/circuit/storage_spec.rb +20 -0
  48. data/spec/lib/circuit/validators_spec.rb +69 -0
  49. data/spec/lib/circuit_spec.rb +139 -0
  50. data/spec/spec_helper.rb +79 -0
  51. data/spec/support/blueprints.rb +24 -0
  52. data/spec/support/matchers/be_current_time_matcher.rb +14 -0
  53. data/spec/support/matchers/extended_have_key.rb +31 -0
  54. data/spec/support/matchers/have_accessor_matcher.rb +13 -0
  55. data/spec/support/matchers/have_attribute_matcher.rb +22 -0
  56. data/spec/support/matchers/have_block_matcher.rb +14 -0
  57. data/spec/support/matchers/have_errors_on_matcher.rb +30 -0
  58. data/spec/support/matchers/have_module_matcher.rb +13 -0
  59. data/spec/support/matchers/have_reader_matcher.rb +18 -0
  60. data/spec/support/matchers/have_writer_matcher.rb +18 -0
  61. data/spec/support/matchers/set_instance_variable.rb +32 -0
  62. data/spec/support/spec_helpers/base_behaviors.rb +20 -0
  63. data/spec/support/spec_helpers/base_models.rb +64 -0
  64. data/spec/support/spec_helpers/logger_helpers.rb +58 -0
  65. data/spec/support/spec_helpers/multi_site_helper.rb +48 -0
  66. data/spec/support/spec_helpers/rack_helpers.rb +8 -0
  67. data/spec/support/spec_helpers/shared_examples/node_store.rb +87 -0
  68. data/spec/support/spec_helpers/shared_examples/site_store.rb +76 -0
  69. data/spec/support/spec_helpers/simple_machinable.rb +29 -0
  70. data/spec/support/spec_helpers/stores_cleaner.rb +46 -0
  71. data/vendor/active_support-3.2/core_ext/string/inflections.rb +53 -0
  72. data/vendor/active_support-3.2/inflector/methods.rb +65 -0
  73. data/vendor/rack-1.4/builder.rb +167 -0
  74. data/vendor/rack-1.4/urlmap.rb +96 -0
  75. metadata +238 -0
@@ -0,0 +1,73 @@
1
+ require 'rack'
2
+ require 'active_support'
3
+ require 'active_support/concern'
4
+ require 'active_support/core_ext/kernel/reporting'
5
+ require 'dionysus/string/version_match'
6
+
7
+ module Circuit
8
+ # Compatibility extensions for Rack 1.3 and Rails 3.1
9
+ module Compatibility
10
+ # Make Rack 1.3 and ActiveSupport 3.1 compatible with circuit.
11
+ # * Overrides Rack::Builder and Rack::URLMap with the classes from Rack 1.4
12
+ # * Adds #demodulize and #deconstantize inflections to ActiveSupport
13
+ def self.make_compatible
14
+ rack13 if ::Rack.release.version_match?("~> 1.3.0")
15
+ active_support31 if ActiveSupport::VERSION::STRING.version_match?("~> 3.1.0")
16
+ end
17
+
18
+ # Include in a model to modify it for compatibility.
19
+ module ActiveModel31
20
+ extend ActiveSupport::Concern
21
+
22
+ # @!method define_attribute_methods(*args)
23
+ # Modified to call `attribute_method_suffix ''` first to create the
24
+ # accessors.
25
+ # @see http://rubydoc.info/gems/activemodel/ActiveModel/AttributeMethods/ClassMethods#define_attribute_methods-instance_method
26
+ # @see http://rubydoc.info/gems/activemodel/ActiveModel/AttributeMethods/ClassMethods#attribute_method_suffix-instance_method
27
+
28
+ included do
29
+ if ActiveModel::VERSION::STRING.version_match?("~> 3.1.0")
30
+ if has_active_model_module?("AttributeMethods")
31
+ class << self
32
+ alias_method_chain :define_attribute_methods, :default_accessor
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ module ClassMethods
41
+ def has_active_model_module?(mod_name)
42
+ included_modules.detect {|mod| mod.to_s == "ActiveModel::#{mod_name.to_s.camelize}"}
43
+ end
44
+
45
+ def define_attribute_methods_with_default_accessor(*args)
46
+ attribute_method_suffix ''
47
+ define_attribute_methods_without_default_accessor(*args)
48
+ end
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def self.vendor_path
55
+ Circuit.vendor_path
56
+ end
57
+
58
+ def self.rack13
59
+ require "rack/urlmap"
60
+ require "rack/builder"
61
+
62
+ require vendor_path.join("rack-1.4", "builder").to_s
63
+ silence_warnings { require vendor_path.join("rack-1.4", "urlmap").to_s }
64
+ end
65
+
66
+ def self.active_support31
67
+ require "active_support/inflector"
68
+
69
+ require vendor_path.join("active_support-3.2", "inflector", "methods").to_s
70
+ require vendor_path.join("active_support-3.2", "core_ext", "string", "inflections").to_s
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,6 @@
1
+ module Circuit
2
+ # Set of predefined middlewares to use with Circuit
3
+ module Middleware
4
+ autoload :Rewriter, "circuit/middleware/rewriter"
5
+ end
6
+ end
@@ -0,0 +1,43 @@
1
+ module Circuit
2
+ module Middleware
3
+ # Raise if rewriting fails.
4
+ class RewriteError < CircuitError ; end
5
+
6
+ # Rewriter middleware
7
+ # @example Use the middleware (in rackup)
8
+ # use Rewriter do |script_name, path_info|
9
+ # ["/pages", script_name+path_info]
10
+ # end
11
+ # @example Use the middleware with Rack::Request object (in rackup)
12
+ # use Rewriter do |request|
13
+ # ["/site/#{request.site.id}"+request.script_name, request.path_info]
14
+ # end
15
+ # @see http://rubydoc.info/gems/rack/Rack/Request Rack::Request documentation
16
+ class Rewriter
17
+ # @param [#call] app Rack app
18
+ # @yield [script_name, path_info] `SCRIPT_NAME` and `PATH_INF`O values
19
+ # @yield [Request] `Rack::Request` object
20
+ # @yieldreturn [Array<String>] new `script_name` and `path_info`
21
+ def initialize(app, &block)
22
+ @app = app
23
+ @block = block
24
+ end
25
+
26
+ # Executes the rewrite
27
+ def call(env)
28
+ begin
29
+ request = ::Rack::Request.new(env)
30
+ script_name, path_info, path = request.script_name, request.path_info, request.path
31
+ env["SCRIPT_NAME"], env["PATH_INFO"] = @block.call(script_name, path_info)
32
+ if script_name != env["SCRIPT_NAME"] or path_info != env["PATH_INFO"]
33
+ ::Circuit.logger.info("[CIRCUIT] Rewriting: '#{path}'->'#{request.path}'")
34
+ end
35
+ rescue RewriteError => ex
36
+ headline = "[CIRCUIT] Rewrite Error"
37
+ ::Circuit.logger.error("%s: %s\n%s %s"%[headline, ex.message, " "*headline.length, ex.backtrace.first])
38
+ end
39
+ @app.call(env)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,14 @@
1
+ require 'rack/request'
2
+
3
+ module Circuit
4
+ module Rack
5
+ require 'circuit/rack/request'
6
+ ::Rack::Request.send(:include, Request)
7
+
8
+ require 'circuit/rack/builder'
9
+ ::Rack::Builder.send(:include, BuilderExt)
10
+
11
+ autoload :Behavioral, 'circuit/rack/behavioral'
12
+ autoload :MultiSite, 'circuit/rack/multi_site'
13
+ end
14
+ end
@@ -0,0 +1,45 @@
1
+ module Circuit
2
+ module Rack
3
+ # Raised if the `rack.circuit.site` rack variable is not defined (or `nil`)
4
+ class MissingSiteError < CircuitError; end
5
+
6
+ # Finds the route (Array of Nodes) for the request, and executes the
7
+ # route's behavior.
8
+ class Behavioral
9
+ def initialize(app)
10
+ @app = app
11
+ end
12
+
13
+ def call(env)
14
+ request = ::Rack::Request.new(env)
15
+
16
+ unless request.site
17
+ raise MissingSiteError, "Rack variable %s is missing"%[::Rack::Request::ENV_SITE]
18
+ end
19
+
20
+ remap! request
21
+ request.route.last.behavior.builder.tap do |builder|
22
+ builder.run(@app) unless builder.app?
23
+ end.call(env)
24
+ end
25
+
26
+ private
27
+
28
+ def remap(request)
29
+ route = ::Circuit.node_store.get(request.site, request.path)
30
+ return nil if route.blank?
31
+
32
+ request.route = route
33
+ return request
34
+ end
35
+
36
+ def remap!(request)
37
+ if result = remap(request)
38
+ result
39
+ else
40
+ raise ::Circuit::Storage::Nodes::NotFoundError, "Path not found"
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,50 @@
1
+ require 'rack/server'
2
+ require 'rack/builder'
3
+
4
+ module Circuit
5
+ module Rack
6
+ # Extensions to Rack::Builder
7
+ module BuilderExt
8
+ # @return [Boolean] true if a default app is set
9
+ def app?
10
+ !!@run
11
+ end
12
+
13
+ # Duplicates the `@use` Array and `@map` Hash instance variables
14
+ def initialize_copy(other)
15
+ @use = other.instance_variable_get(:@use).dup
16
+ unless other.instance_variable_get(:@map).nil?
17
+ @map = other.instance_variable_get(:@map).dup
18
+ end
19
+ end
20
+ end
21
+
22
+ # A Rack::Builder variation that does not require a fully-compliant rackup
23
+ # file; specifically that a default app (`run` directive) is not required.
24
+ class Builder < ::Rack::Builder
25
+ include BuilderExt
26
+
27
+ # Parses the rackup (or circuit-rackup .cru) file.
28
+ # @return [Circuit::Rack::Builder] the builder
29
+ def self.parse_file(config, opts = ::Rack::Server::Options.new)
30
+ # allow for objects that are String-like but don't respond to =~
31
+ # (e.g. Pathname)
32
+ config = config.to_s
33
+
34
+ if config.to_s =~ /\.cru$/
35
+ options = {}
36
+ cfgfile = ::File.read(config)
37
+ cfgfile.sub!(/^__END__\n.*\Z/m, '')
38
+ builder = eval "%s.new {\n%s\n}"%[self, cfgfile], TOPLEVEL_BINDING, config
39
+ return builder, options
40
+ else
41
+ # this should be a fully-compliant rackup file (or a constant name),
42
+ # so use the real Rack::Builder, but return a Builder object instead
43
+ # of the app
44
+ app, options = ::Rack::Builder.parse_file(config, opts)
45
+ return self.new(app), options
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,22 @@
1
+ module Circuit
2
+ module Rack
3
+ # Finds the Circuit::Site for the request. Returns a 404 if the site is not found.
4
+ class MultiSite
5
+ def initialize(app)
6
+ @app = app
7
+ end
8
+
9
+ def call(env)
10
+ request = ::Rack::Request.new(env)
11
+
12
+ request.site = Circuit.site_store.get(request.host)
13
+ unless request.site
14
+ # TODO custom 404 page for site not found
15
+ return [404, {}, ["Not Found"]]
16
+ end
17
+
18
+ @app.call(request.env)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,81 @@
1
+ require 'active_support/concern'
2
+
3
+ module Circuit
4
+ module Rack
5
+ # Extensions made to the ::Rack::Request class
6
+ # @see http://rubydoc.info/gems/rack/Rack/Request ::Rack::Request API documentation
7
+ # @see ClassMethods
8
+ module Request
9
+ extend ActiveSupport::Concern
10
+
11
+ # Key for storing the Circuit::Site instance
12
+ ENV_SITE = 'rack.circuit.site'
13
+
14
+ # Key for storing the array of Circuit::Node instances
15
+ ENV_ROUTE = 'rack.circuit.route'
16
+
17
+ # Key for storing original parameters that Circuit modifies.
18
+ ENV_ORIGINALS = 'rack.circuit.originals'
19
+
20
+ module ClassMethods
21
+ # Parses a URI path String into its segments
22
+ # @param [String] path to parse
23
+ # @return [Array<String>] segment parts; first segment will be `nil` if
24
+ # it is the root (i.e. the path is absolute)
25
+ # @see http://tools.ietf.org/html/rfc2396#section-3.3 RFC 2396: Uniform
26
+ # Resource Identifiers (URI): Generic Syntax - Path Component
27
+ def path_segments(path)
28
+ path.gsub(/^\/+/,'').split(/\/+/).select { |seg| !seg.blank? }.tap do |result|
29
+ result.unshift nil if path =~ /^\//
30
+ end
31
+ end
32
+ end
33
+
34
+ # @return [Circuit::Site] site from the environment
35
+ def site() @env[ENV_SITE]; end
36
+
37
+ # @param [Circuit::Site] site to set into the environment
38
+ def site=(site) @env[ENV_SITE] = site; end
39
+
40
+ # @return [Array<Circuit::Node>] the array of nodes that make up the
41
+ # route
42
+ def route() @env[ENV_ROUTE]; end
43
+
44
+ # Sets the route and modifies the `PATH_INFO` and `SCRIPT_NAME` variables
45
+ # to conform to the route. This means that that route's path will be set
46
+ # as the `SCRIPT_NAME` and the remainder of the path will be set as the
47
+ # `PATH_INFO`. Also, the original `PATH_INFO` and `SCRIPT_NAME`
48
+ # values are copied into the originals key.
49
+ # @param [Array<Circuit::Node>] route the array of nodes that make up the route
50
+ def route=(route)
51
+ raise(Circuit::Error, "Route has already been set") if self.route
52
+ save_original_path_envs
53
+ @env["PATH_INFO"] = "/"+path_segments[route.length..-1].join("/")
54
+ @env["PATH_INFO"] = "" if @env["PATH_INFO"] == "/"
55
+ @env["SCRIPT_NAME"] = route.last.path
56
+ @env[ENV_ROUTE] = route;
57
+ end
58
+
59
+ # @return [String] the route's path (aka. the `SCRIPT_NAME`)
60
+ def route_path() @env["SCRIPT_NAME"]; end
61
+
62
+ # @return [Hash] the originals of any keys that Circuit modifies are
63
+ # first copied into this Hash
64
+ def circuit_originals() @env[ENV_ORIGINALS] ||= {}; end
65
+
66
+ # @return [Array<String>] segment parts; first segment will be `nil` if
67
+ # it is the root (i.e. the path is absolute)
68
+ # @see ClassMethods#path_segments
69
+ def path_segments
70
+ self.class.path_segments self.path
71
+ end
72
+
73
+ # Saves the original path keys into #circuit_originals (i.e. `PATH_INFO`
74
+ # and `SCRIPT_NAME`)
75
+ def save_original_path_envs
76
+ circuit_originals["PATH_INFO"] = @env["PATH_INFO"]
77
+ circuit_originals["SCRIPT_NAME"] = @env["SCRIPT_NAME"]
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,24 @@
1
+ module Circuit
2
+ # Initializer `circuit_railtie.configure_rails_initialization`
3
+ # ------------------------------------------------------------
4
+ # * adds Rack::MultiSite and Rack::Behavioral to the Rails middleware stack
5
+ # * Sets the Circuit logger to the Rails logger
6
+ class Railtie < Rails::Railtie
7
+ initializer "circuit_railtie.configure_rails_initialization" do |app|
8
+ app.middleware.insert 0, Rack::MultiSite
9
+ app.middleware.insert 1, Rack::Behavioral
10
+
11
+ unless Circuit.cru_path
12
+ Circuit.cru_path = app.root.join("app", "behaviors")
13
+ end
14
+
15
+ root = app.root.expand_path.to_s
16
+ rel = Circuit.cru_path.expand_path.to_s.gsub(/^#{Regexp.escape(root)}\//, "")
17
+ app.config.paths.add rel, :eager_load => true, :glob => "**/*.rb"
18
+
19
+ app.config.after_initialize do
20
+ Circuit.logger = Rails.logger
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,74 @@
1
+ require 'active_support/inflector'
2
+
3
+ module Circuit
4
+ module Storage
5
+ autoload :MemoryModel, 'circuit/storage/memory_model'
6
+ autoload :Sites, 'circuit/storage/sites'
7
+ autoload :Nodes, 'circuit/storage/nodes'
8
+
9
+ # Raised if a storage instance is undefined.
10
+ class InstanceUndefinedError < CircuitError
11
+ def initialize(msg="Storage instance is undefined.")
12
+ super(msg)
13
+ end
14
+ end
15
+
16
+ # @raise InstanceUndefinedError if the instance isn't defined
17
+ # @return [Circuit::Storage::Nodes::BaseStore,Circuit::Storage::Sites::BaseStore]
18
+ # the storage instance
19
+ def instance
20
+ @instance || raise(InstanceUndefinedError)
21
+ end
22
+
23
+ # Set the storage instance and alias the `Node` or `Site` model under
24
+ # `Circuit` as `Circuit::Node` or `Circuit::Site`.
25
+ #
26
+ # @raise ArgumentError if the storage instance or the `Site` or `Node`
27
+ # model cannot be determined
28
+ # @overload set_instance(instance)
29
+ # @param [Circuit::Storage::Nodes::BaseStore,Circuit::Storage::Sites::BaseStore]
30
+ # instance storage instance
31
+ # @return [Circuit::Storage::Nodes::BaseStore,Circuit::Storage::Sites::BaseStore]
32
+ # storage instance
33
+ # @overload set_instance(klass_or_name, *args)
34
+ # @param [Class,String,Symbol] klass_or_name class or name of class
35
+ # (under Circuit::Storage::Nodes or
36
+ # Circuit::Storage::Sites) for storage
37
+ # instance
38
+ # @param [Array] args any arguments to instantiate the storage instance
39
+ # @return [Circuit::Storage::Nodes::BaseStore,Circuit::Storage::Sites::BaseStore]
40
+ # storage instance
41
+ def set_instance(*args)
42
+ klass = nil
43
+
44
+ case args.first
45
+ when Nodes::BaseStore, Sites::BaseStore
46
+ @instance = args.first
47
+ when Class
48
+ klass = args.first
49
+ when String, Symbol
50
+ # TODO do we need to fall up the module hiearchy to find the constant?
51
+ # (e.g. a store defined in the Kernel namespace?)
52
+ klass = const_get(args.first.to_s.camelize)
53
+ else
54
+ raise ArgumentError, "Unexpected type for storage instance: %s"%[args.first.class]
55
+ end
56
+
57
+ if klass
58
+ @instance = klass.new(*args[1..-1])
59
+ end
60
+
61
+ case @instance
62
+ when Nodes::BaseStore
63
+ ::Circuit.const_set(:Node, @instance.class.const_get(:Node))
64
+ when Sites::BaseStore
65
+ ::Circuit.const_set(:Site, @instance.class.const_get(:Site))
66
+ else
67
+ bad_instance = @instance; @instance = nil
68
+ raise ArgumentError, "Cannot determine a Site or Node class for storage type: %s"%[bad_instance.class]
69
+ end
70
+
71
+ @instance
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,70 @@
1
+ require 'active_support/concern'
2
+ require 'active_model/naming'
3
+ require 'active_model/attribute_methods'
4
+
5
+ module Circuit
6
+ module Storage
7
+ module MemoryModel
8
+ extend ActiveSupport::Concern
9
+
10
+ include ActiveModel::AttributeMethods
11
+ include Compatibility::ActiveModel31
12
+ attr_reader :attributes, :errors
13
+ attr_accessor :name
14
+
15
+ included do
16
+ extend ActiveModel::Naming
17
+ class_attribute :all
18
+ self.all = Array.new
19
+ end
20
+
21
+ module ClassMethods
22
+ def setup_attributes(*attrs)
23
+ attribute_method_suffix '?'
24
+ attribute_method_suffix '='
25
+ define_attribute_methods attrs.collect(&:to_sym)
26
+ end
27
+ end
28
+
29
+ def attributes=(hash)
30
+ @attributes = hash.with_indifferent_access
31
+ end
32
+
33
+ def save!
34
+ self.save ? true : raise("Invalid %s: %p"%[self.class, self.errors])
35
+ end
36
+
37
+ def persisted?
38
+ @persisted
39
+ end
40
+
41
+ def persisted!(val=true)
42
+ @persisted = val
43
+ end
44
+
45
+ def eql?(obj)
46
+ obj.instance_of?(self.class) && obj.attributes == self.attributes
47
+ end
48
+
49
+ protected
50
+
51
+ def attribute(key)
52
+ attributes[key]
53
+ end
54
+
55
+ def attribute=(key, val)
56
+ attributes[key] = val
57
+ end
58
+
59
+ def attribute?(attr)
60
+ !attributes[attr.to_sym].blank?
61
+ end
62
+
63
+ def memory_model_setup
64
+ @persisted = false
65
+ @attributes = HashWithIndifferentAccess.new
66
+ @errors = ActiveModel::Errors.new(self)
67
+ end
68
+ end
69
+ end
70
+ end