replay 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rvmrc +1 -1
- data/Gemfile +7 -3
- data/Guardfile +10 -0
- data/LICENSE +21 -0
- data/README.md +153 -0
- data/Rakefile +15 -0
- data/lib/replay.rb +39 -10
- data/lib/replay/backends.rb +50 -0
- data/lib/replay/event_declarations.rb +36 -0
- data/lib/replay/event_decorator.rb +13 -0
- data/lib/replay/events.rb +24 -0
- data/lib/replay/inflector.rb +55 -0
- data/lib/replay/observer.rb +18 -0
- data/lib/replay/publisher.rb +72 -0
- data/lib/replay/repository.rb +61 -0
- data/lib/replay/repository/configuration.rb +30 -0
- data/lib/replay/repository/identity_map.rb +25 -0
- data/lib/replay/router.rb +5 -0
- data/lib/replay/router/default_router.rb +21 -0
- data/lib/replay/rspec.rb +50 -0
- data/lib/replay/subscription_manager.rb +28 -0
- data/lib/replay/test.rb +64 -0
- data/lib/replay/test/test_event_stream.rb +19 -0
- data/lib/replay/version.rb +1 -1
- data/proofs/all.rb +7 -0
- data/proofs/proofs_init.rb +10 -0
- data/proofs/replay/inflector_proof.rb +32 -0
- data/proofs/replay/publisher_proof.rb +170 -0
- data/proofs/replay/repository_configuration_proof.rb +67 -0
- data/proofs/replay/repository_proof.rb +46 -0
- data/proofs/replay/subscriber_manager_proof.rb +39 -0
- data/proofs/replay/test_proof.rb +28 -0
- data/replay.gemspec +5 -4
- data/test/replay/observer_spec.rb +37 -0
- data/test/replay/router/default_router_spec.rb +43 -0
- data/test/test_helper.rb +10 -0
- metadata +65 -48
- data/README +0 -27
- data/lib/replay/active_record_event_store.rb +0 -32
- data/lib/replay/domain.rb +0 -33
- data/lib/replay/event.rb +0 -27
- data/lib/replay/event_store.rb +0 -55
- data/lib/replay/projector.rb +0 -19
- data/lib/replay/test_storage.rb +0 -8
- data/lib/replay/unknown_event_error.rb +0 -2
- data/test/spec_helper.rb +0 -6
- data/test/test_events.sqlite3 +0 -0
- data/test/unit/active_record_event_store_spec.rb +0 -24
- data/test/unit/domain_spec.rb +0 -53
- data/test/unit/event_spec.rb +0 -13
- data/test/unit/event_store_spec.rb +0 -28
- data/test/unit/projector_spec.rb +0 -19
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 1c2e196bd8e54aa363a8ad762f41797aa99c989e
|
4
|
+
data.tar.gz: cda39f5a62f384de3ba509f291407a54035b007b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: af511f51f8dbec4f12fc284bebf4232786081e6832089d1bd7209f7ab88b001e02a40bb85a7182acb562c19715c037c797c0e5be4d54b4bfd51228e9bb853c94
|
7
|
+
data.tar.gz: 25ff65cea036543f63088c7ff299acbd1128cbbfd70c74aa6fbfefa4fa4c9312a0b034ac687cecff2e4ef041c063aac18ec5956d897d9f2d63da3c71c8351a57
|
data/.rvmrc
CHANGED
@@ -1 +1 @@
|
|
1
|
-
rvm use
|
1
|
+
rvm use 2.0.0@replay --create
|
data/Gemfile
CHANGED
@@ -3,8 +3,12 @@ source "http://rubygems.org"
|
|
3
3
|
# Specify your gem's dependencies in replay.gemspec
|
4
4
|
gemspec
|
5
5
|
|
6
|
+
group :development do
|
7
|
+
gem 'guard'
|
8
|
+
gem 'guard-shell'
|
9
|
+
end
|
10
|
+
|
6
11
|
group :test do
|
7
|
-
gem "
|
8
|
-
gem
|
9
|
-
gem "mocha"
|
12
|
+
gem "proof", :git => 'https://github.com/Sans/proof.git'
|
13
|
+
gem 'byebug'
|
10
14
|
end
|
data/Guardfile
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
# Add files and commands to this file, like the example:
|
5
|
+
# watch(%r{file/path}) { `command(s)` }
|
6
|
+
#
|
7
|
+
guard :shell, :all_on_start => false do
|
8
|
+
watch(/lib\/(.*).rb/) {|m| `ruby proofs/all.rb` }
|
9
|
+
watch(/proofs\/(.*).rb/) {|m| `ruby #{m[0]}`}
|
10
|
+
end
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2014 Keith Gaddis
|
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,153 @@
|
|
1
|
+
#Replay
|
2
|
+
Replay is a gem to support event sourcing data within domain boundaries. With event sourced data, the application stores data as a series of domain events, which are applied to the domain object in order to mutate state.
|
3
|
+
|
4
|
+
###CQRS/ES 30 second intro
|
5
|
+
[Command Query Responsibility Segregation](http://codebetter.com/gregyoung/2010/02/16/cqrs-task-based-uis-event-sourcing-agh/) (and [Fowler's explanation](http://martinfowler.com/bliki/CQRS.html) is a pattern popularized by Greg Young and Udi Dahan from within the sphere of Domain Driven Design. The general idea is that within domain models, objects are rarely good at both representing truth and being purposeful for queries and reporting, and therefore we should separate the responsibilities.
|
6
|
+
|
7
|
+
[Event Sourcing](http://martinfowler.com/eaaDev/EventSourcing.html) is a pattern that is not required by (but pairs extremely well with) CQRS. However, by embracing this pattern a system can adapt to new reporting and query requirements at any time with a great deal of flexibility, and the use of messaging/pub-sub along with events creates an easy path to breaking apart monolithic applications and separating domains.
|
8
|
+
|
9
|
+
###A short example
|
10
|
+
|
11
|
+
class ReplayExample
|
12
|
+
include Replay::Publisher
|
13
|
+
|
14
|
+
#define events
|
15
|
+
events do
|
16
|
+
SomethingHappened(name: String, pid: Integer)
|
17
|
+
SomethingElseHappened(pid: Integer)
|
18
|
+
#....
|
19
|
+
end
|
20
|
+
|
21
|
+
#applying events (changing state)
|
22
|
+
apply SomethingHappened do |event|
|
23
|
+
@name = event.name
|
24
|
+
@pid = event.pid
|
25
|
+
end
|
26
|
+
|
27
|
+
apply SomethingElseHappened do |event|
|
28
|
+
@state = :happened_again
|
29
|
+
@pid = nil if event.pid == @pid
|
30
|
+
end
|
31
|
+
|
32
|
+
def do_something(pid = nil)
|
33
|
+
#the command validates inputs
|
34
|
+
#InvalidCommand is defined by the application
|
35
|
+
raise InvalidCommand.new("parameters were invalid") unless pid
|
36
|
+
|
37
|
+
#publish events
|
38
|
+
publish SomethingHappened.new(:name = "foo", :pid => pid)
|
39
|
+
|
40
|
+
#publish with method syntax
|
41
|
+
publish SomethingElseHappened(pid: pid)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
There's a couple of things to note about the above example. ReplayExample is a domain object. (Clearly this example is a bit contrived.) [Domain objects](http://martinfowler.com/eaaCatalog/domainModel.html) represent and encapsulate domain logic in its purest sense. No application code should make its way into a domain object, nor should concerns from another bounded context.
|
46
|
+
|
47
|
+
Domain objects publish events in order to mutate state. The events published by this domain object are defined within the `events` block; `ReplayExample::SomethingHappened` is a class defined there, which has two attributes `name` and `pid`, which are `String` and `Integer` respectively. Events may also be defined manually, like any other class. Because they're essentially value objects, with zero behavior, the shorthand form above is usually going to be easier.
|
48
|
+
|
49
|
+
`ReplayExample` instances change state by applying events. These events are handled in the `apply` blocks in the above example (you see what I did there?) This part is, mostly, really simple. You probably did a lot of this state thing in your freshman programming class. More on that later.
|
50
|
+
|
51
|
+
So if we've got the events defined, and we know what events change state in which ways, where do they come from? Commands, of course. The role of a command is to validate its inputs and publish the events if the command is valid. That's it. No changing state allowed there—seriously, none. Ever heard the term [snowflake server](http://martinfowler.com/bliki/SnowflakeServer.html)? Break the state rule and you're going to have snowflake instances and weird bugs.
|
52
|
+
|
53
|
+
Commands are the art and science of CQRS. In the above example, I've implemented it as a method on the domain object (which is also called an aggregate root in the language of DDD.) Its just as frequently done as a class, e.g.
|
54
|
+
|
55
|
+
class ReplayExample::DoSomething
|
56
|
+
include Replay::Publisher
|
57
|
+
|
58
|
+
def initialize(name, pid=nil)
|
59
|
+
raise InvalidCommand.new unless pid
|
60
|
+
@name = name
|
61
|
+
@pid = pid
|
62
|
+
end
|
63
|
+
|
64
|
+
def perform
|
65
|
+
#the publish the event, but don't raise an error if an application block can't be found
|
66
|
+
publish ReplayExample::SomethingHappened.new(name: @name, pid: @pid), false
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
ReplayExample::DoSomething.new("foo", 123).perform
|
71
|
+
|
72
|
+
The above command class performs the same function, but has some advantages. In a Rails application, you can mix in ActiveModel::Validations to get ActiveRecord-style validators on it. You can also use Virtus (recommended) or ActiveModel to make it ActiveModel compliant and use it as a form object. This pattern is especially useful when you're dealing with non-domain services (e.g. credit card processors.) You can publish events from any model; there's nothing special about that (though its best if you don't do it without good reason, or you'll subvert one of the great advantages of DDD—separation of bounded contexts).
|
73
|
+
|
74
|
+
##Digging deeper
|
75
|
+
|
76
|
+
###The Repository
|
77
|
+
The Repository is an application-defined object (replay will generate one for you) which will load your domain objects from storage. The repository's job is to find the event stream requested and apply the events from the event stream to a newly created object of the supplied type. Every application has at least one repository, and may have several.
|
78
|
+
|
79
|
+
Use it like so:
|
80
|
+
|
81
|
+
example = Repository.load(ReplayExample, some_guid)
|
82
|
+
|
83
|
+
What you'll get back is a newly initialized instance of your object, with all events from the stream applied in sequence. By default, if it doesn't find any events for that stream identifier, it will raise an exception; you can change this behavior by supplying `:create => false` or `:create => true` to `load`. When false, the Repository will not attempt to create the instance. If true, and the object defines a `create` method that takes no parameters, the default implementation will call `create`. (Its standard practice for that method to publish a `Created` event.)
|
84
|
+
|
85
|
+
Your application's repository will look something like this:
|
86
|
+
|
87
|
+
class Repository
|
88
|
+
include Replay::Repository
|
89
|
+
|
90
|
+
configure do |config|
|
91
|
+
config.store = :active_record
|
92
|
+
config.add_default_subscriber EventLogger
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
You can also create a repository for your test environment (though for unit tests its typically unnecessary and for higher levels adding a subscriber will suffice. For example, in Cucumber or its analogues:
|
97
|
+
|
98
|
+
#features/env.rb
|
99
|
+
Repository.configuration.add_default_listener EventMonitor.new
|
100
|
+
|
101
|
+
###Observers
|
102
|
+
Replay provides a default message router for observers of events.
|
103
|
+
|
104
|
+
In your repository implementation, add :replay_router to the configuration's default subscribers:
|
105
|
+
|
106
|
+
class Repository
|
107
|
+
include Replay::Repository
|
108
|
+
|
109
|
+
configure do |config|
|
110
|
+
config.add_default_subscriber :replay_router
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
In your application or domain services:
|
115
|
+
|
116
|
+
class MailService
|
117
|
+
include Replay::Observer
|
118
|
+
|
119
|
+
observe Model::EventHappened do |event|
|
120
|
+
#handle the event
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
It may be advantageous in some situations to create multiple routers:
|
125
|
+
|
126
|
+
class InternalRouter
|
127
|
+
include Replay::Router
|
128
|
+
end
|
129
|
+
|
130
|
+
class Repository
|
131
|
+
include Replay::Repository
|
132
|
+
|
133
|
+
configure do |config|
|
134
|
+
config.add_default_subscriber InternalRouter
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
class MailService
|
139
|
+
include Replay::Observer
|
140
|
+
router InternalRouter
|
141
|
+
|
142
|
+
#observations...
|
143
|
+
end
|
144
|
+
|
145
|
+
##Additional gems
|
146
|
+
|
147
|
+
[replay-rails](http://github.com/karmajunkie/replay-rails) provides a very basic ActiveRecord-based event store. Its a good template for building your own event store and light duties in an application in which aggregates don't receive hundreds or thousands of events.
|
148
|
+
|
149
|
+
|
150
|
+
##TODO
|
151
|
+
* Implement snapshots for efficient load from repository
|
152
|
+
* Better documentation
|
153
|
+
* Build a demonstration app
|
data/Rakefile
CHANGED
@@ -1 +1,16 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
|
+
require 'rake/testtask'
|
3
|
+
|
4
|
+
task :prove_all do
|
5
|
+
Bundler.setup
|
6
|
+
Bundler.require :default
|
7
|
+
require_relative "proofs/all"
|
8
|
+
end
|
9
|
+
Rake::TestTask.new do |t|
|
10
|
+
t.libs.push "lib"
|
11
|
+
t.libs.push "test"
|
12
|
+
t.test_files = FileList['test/**/*_spec.rb']
|
13
|
+
t.verbose = true
|
14
|
+
end
|
15
|
+
|
16
|
+
task :default => [:test, :prove_all]
|
data/lib/replay.rb
CHANGED
@@ -1,14 +1,43 @@
|
|
1
|
-
require
|
2
|
-
require 'active_support/concern'
|
3
|
-
require 'replay/unknown_event_error'
|
4
|
-
require 'replay/test_storage'
|
5
|
-
require 'replay/configuration'
|
6
|
-
require 'replay/event'
|
7
|
-
require 'replay/event_store'
|
8
|
-
require 'replay/active_record_event_store'
|
9
|
-
require 'replay/domain'
|
10
|
-
require 'replay/projector'
|
1
|
+
require 'virtus'
|
11
2
|
|
12
3
|
module Replay
|
4
|
+
def self.logger=(logger)
|
5
|
+
@logger = logger
|
6
|
+
end
|
13
7
|
|
8
|
+
def self.logger
|
9
|
+
@logger
|
10
|
+
end
|
11
|
+
|
12
|
+
class ReplayError < StandardError; end
|
13
|
+
class UndefinedKeyError < ReplayError; end
|
14
|
+
class UnhandledEventError < ReplayError; end
|
15
|
+
class UnknownEventError < ReplayError; end
|
16
|
+
class InvalidStorageError < ReplayError;
|
17
|
+
def initialize(*args)
|
18
|
+
klass = args.shift
|
19
|
+
super( "Storage #{klass.to_s} does not implement #event_stream(stream, event)", *args)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
class InvalidSubscriberError < ReplayError;
|
23
|
+
def initialize(*args)
|
24
|
+
obj = args.shift
|
25
|
+
super( "Subscriber#{obj.to_s} does not implement #published(stream, event)", *args)
|
26
|
+
end
|
27
|
+
end
|
14
28
|
end
|
29
|
+
|
30
|
+
require 'replay/inflector'
|
31
|
+
require 'replay/events'
|
32
|
+
require 'replay/event_decorator'
|
33
|
+
require 'replay/event_declarations'
|
34
|
+
require 'replay/publisher'
|
35
|
+
require 'replay/subscription_manager'
|
36
|
+
require 'replay/backends'
|
37
|
+
require 'replay/repository'
|
38
|
+
require 'replay/repository/identity_map'
|
39
|
+
require 'replay/repository/configuration'
|
40
|
+
require 'replay/observer'
|
41
|
+
require 'replay/router'
|
42
|
+
require 'replay/router/default_router'
|
43
|
+
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
module Replay
|
3
|
+
class Backends
|
4
|
+
def self.register(shorthand, klass)
|
5
|
+
@backends ||= {}
|
6
|
+
@backends[shorthand] = klass
|
7
|
+
return klass
|
8
|
+
end
|
9
|
+
def self.resolve(shorthand)
|
10
|
+
@backends[shorthand] || shorthand
|
11
|
+
end
|
12
|
+
|
13
|
+
class MemoryStore
|
14
|
+
include Singleton
|
15
|
+
def initialize
|
16
|
+
@store = {}
|
17
|
+
end
|
18
|
+
def self.published(stream_id, event)
|
19
|
+
instance.published(stream_id, event)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.clear
|
23
|
+
instance.clear
|
24
|
+
end
|
25
|
+
|
26
|
+
def clear
|
27
|
+
@store = {}
|
28
|
+
end
|
29
|
+
|
30
|
+
def published(stream_id, event)
|
31
|
+
@store[stream_id] ||= []
|
32
|
+
@store[stream_id] << event
|
33
|
+
end
|
34
|
+
|
35
|
+
def event_stream(stream_id)
|
36
|
+
@store[stream_id] || []
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.event_stream(stream_id)
|
40
|
+
instance.event_stream(stream_id)
|
41
|
+
end
|
42
|
+
def self.[](stream_id)
|
43
|
+
instance.event_stream(stream_id)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
register :memory, MemoryStore
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Replay
|
2
|
+
module EventDeclarations
|
3
|
+
def self.included(base)
|
4
|
+
base.extend(Replay::Events)
|
5
|
+
end
|
6
|
+
|
7
|
+
def included(base)
|
8
|
+
self.constants.each do |c|
|
9
|
+
base.const_set(c, const_get(c).dup)
|
10
|
+
klass = base.const_get(c)
|
11
|
+
base.class_eval do
|
12
|
+
define_method c do |props|
|
13
|
+
klass.new props
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
def method_missing(name, *args)
|
19
|
+
declare_event(self, name, args.first)
|
20
|
+
end
|
21
|
+
|
22
|
+
def declare_event(base, name, props)
|
23
|
+
klass = Class.new do
|
24
|
+
include Replay::EventDecorator
|
25
|
+
attribute :published_at, Time, default: lambda{|p,a| Time.now}
|
26
|
+
values do
|
27
|
+
props.keys.each do |prop|
|
28
|
+
attribute prop, props[prop]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
include Virtus::Equalizer.new("#{name.to_s} equalizer", (self.attribute_set.map(&:name) - [:published_at]).map(&:to_s))
|
32
|
+
end
|
33
|
+
base.const_set name, klass
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Replay
|
2
|
+
#hook class to apply global decorators to events
|
3
|
+
module EventDecorator
|
4
|
+
def self.included(base)
|
5
|
+
base.class_eval do
|
6
|
+
include Virtus.value_object
|
7
|
+
def inspect
|
8
|
+
"#{self.class.to_s}: #{self.attributes.map{|k, v| "#{k.to_s} = #{v.to_s}"}.join(", ")}"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
|
2
|
+
module Replay
|
3
|
+
module Events
|
4
|
+
def self.extended(base)
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
end
|
7
|
+
def self.included(base)
|
8
|
+
base.extend(ClassMethods)
|
9
|
+
#self.constants.each{|c| base.const_set(c, const_get(c))}
|
10
|
+
end
|
11
|
+
module ClassMethods
|
12
|
+
def events(mod = nil, &block)
|
13
|
+
unless mod
|
14
|
+
mod = Module.new do
|
15
|
+
extend Replay::EventDeclarations
|
16
|
+
module_eval &block
|
17
|
+
end
|
18
|
+
self.const_set(:Events, mod)
|
19
|
+
end
|
20
|
+
include mod
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
#This class substantially copied from ActiveSupport, licensed under MIT
|
2
|
+
#Their version is generally better, so use that unless you've got a good reason
|
3
|
+
#not to do so. also, i've changed certain aspects of behavior.
|
4
|
+
class Replay::Inflector
|
5
|
+
|
6
|
+
# By default, +camelize+ converts strings to UpperCamelCase. If the argument
|
7
|
+
# to +camelize+ is set to <tt>:lower</tt> then +camelize+ produces
|
8
|
+
# lowerCamelCase.
|
9
|
+
#
|
10
|
+
# +camelize+ will also convert '/' to '::' which is useful for converting
|
11
|
+
# paths to namespaces.
|
12
|
+
#
|
13
|
+
# 'active_model'.camelize # => "ActiveModel"
|
14
|
+
# 'active_model'.camelize(:lower) # => "activeModel"
|
15
|
+
# 'active_model/errors'.camelize # => "ActiveModel::Errors"
|
16
|
+
# 'active_model/errors'.camelize(:lower) # => "activeModel::Errors"
|
17
|
+
#
|
18
|
+
# As a rule of thumb you can think of +camelize+ as the inverse of
|
19
|
+
# +underscore+, though there are cases where that does not hold:
|
20
|
+
#
|
21
|
+
# 'SSLError'.underscore.camelize # => "SslError"
|
22
|
+
def self.camelize(term, uppercase_first_letter = false)
|
23
|
+
string = term.to_s
|
24
|
+
string = string.sub(/^[A-Z_]/) { $&.downcase }
|
25
|
+
string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{$2.capitalize}" }
|
26
|
+
string.gsub!('/', '::')
|
27
|
+
string.gsub!('.', '::')
|
28
|
+
string
|
29
|
+
end
|
30
|
+
# Makes an underscored, lowercase form from the expression in the string.
|
31
|
+
#
|
32
|
+
# Changes '::' to '/' to convert namespaces to paths.
|
33
|
+
#
|
34
|
+
# 'ActiveModel'.underscore # => "active_model"
|
35
|
+
# 'ActiveModel::Errors'.underscore # => "active_model/errors"
|
36
|
+
#
|
37
|
+
# As a rule of thumb you can think of +underscore+ as the inverse of
|
38
|
+
# +camelize+, though there are cases where that does not hold:
|
39
|
+
#
|
40
|
+
# 'SSLError'.underscore.camelize # => "SslError"
|
41
|
+
def self.underscore(camel_cased_word)
|
42
|
+
return camel_cased_word unless camel_cased_word =~ /[A-Z-]|::/
|
43
|
+
word = camel_cased_word.to_s.gsub('::', '.')
|
44
|
+
word.gsub!(/(?:([A-Za-z\d])^)(?=\b|[^a-z])/) { "#{$1}#{$2 && '_'}#{$2.downcase}" }
|
45
|
+
word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
|
46
|
+
word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
|
47
|
+
word.tr!("-", "_")
|
48
|
+
word.downcase!
|
49
|
+
word
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.constantize(class_name)
|
53
|
+
class_name.to_s.split("::").inject(Kernel){|parent, mod| parent.const_get(mod)}
|
54
|
+
end
|
55
|
+
end
|