agrippa 0.0.1
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 +7 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/Guardfile +7 -0
- data/LICENSE.txt +202 -0
- data/README.md +186 -0
- data/Rakefile +13 -0
- data/agrippa.gemspec +28 -0
- data/lib/agrippa.rb +10 -0
- data/lib/agrippa/accessor_methods.rb +28 -0
- data/lib/agrippa/delegation.rb +67 -0
- data/lib/agrippa/immutable.rb +80 -0
- data/lib/agrippa/maybe.rb +18 -0
- data/lib/agrippa/methods.rb +52 -0
- data/lib/agrippa/mutable.rb +62 -0
- data/lib/agrippa/mutable_hash.rb +72 -0
- data/lib/agrippa/proxy.rb +77 -0
- data/lib/agrippa/state.rb +12 -0
- data/lib/agrippa/utilities.rb +12 -0
- data/lib/agrippa/version.rb +3 -0
- data/spec/delegation_spec.rb +146 -0
- data/spec/immutable_spec.rb +23 -0
- data/spec/maybe_spec.rb +32 -0
- data/spec/methods_spec.rb +38 -0
- data/spec/mutable_hash_spec.rb +14 -0
- data/spec/mutable_spec.rb +14 -0
- data/spec/object_state_spec.rb +38 -0
- data/spec/proxy_spec.rb +107 -0
- data/spec/shared/state_container_examples.rb +150 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/state_spec.rb +17 -0
- data/spec/utilities_spec.rb +21 -0
- metadata +225 -0
data/lib/agrippa.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
require "agrippa/utilities"
|
2
|
+
require "agrippa/version"
|
3
|
+
require "agrippa/methods"
|
4
|
+
require "agrippa/delegation"
|
5
|
+
require "agrippa/proxy"
|
6
|
+
require "agrippa/state"
|
7
|
+
require "agrippa/maybe"
|
8
|
+
require "agrippa/mutable"
|
9
|
+
require "agrippa/mutable_hash"
|
10
|
+
Agrippa::Utilities.try_require "agrippa/immutable"
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Agrippa
|
2
|
+
module AccessorMethods
|
3
|
+
def state_reader(*args)
|
4
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
5
|
+
args.flatten.each do |arg|
|
6
|
+
__define_state_reader(arg, caller, options)
|
7
|
+
end
|
8
|
+
self
|
9
|
+
end
|
10
|
+
|
11
|
+
def state_writer(*args)
|
12
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
13
|
+
args.flatten.each do |arg|
|
14
|
+
__define_state_writer(arg, caller, options)
|
15
|
+
end
|
16
|
+
self
|
17
|
+
end
|
18
|
+
|
19
|
+
def state_accessor(*args)
|
20
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
21
|
+
args.flatten.each do |arg|
|
22
|
+
__define_state_reader(arg, caller, options)
|
23
|
+
__define_state_writer(arg, caller, options)
|
24
|
+
end
|
25
|
+
self
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require "agrippa/methods"
|
2
|
+
|
3
|
+
module Agrippa
|
4
|
+
module Delegation
|
5
|
+
def self.included(base)
|
6
|
+
base.send(:include, Agrippa::Methods)
|
7
|
+
base.send(:extend, ClassMethods)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.interpolate(string, vars)
|
11
|
+
output = string.gsub(/[A-Z]{3,}/) do |match|
|
12
|
+
vars.fetch(match.downcase.to_sym)
|
13
|
+
end
|
14
|
+
output.lines.map(&:strip).join("; ")
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.build(base, delegate_caller, methods, definition)
|
18
|
+
options = methods.last.is_a?(Hash) ? methods.pop.dup : {}
|
19
|
+
target = options.delete(:to)
|
20
|
+
target = "self.class" if (target.to_sym == :class)
|
21
|
+
raise(ArgumentError) unless target
|
22
|
+
|
23
|
+
options[:prefix] = target if (options[:prefix] == true)
|
24
|
+
options[:suffix] = target if (options[:suffix] == true)
|
25
|
+
|
26
|
+
file, line = delegate_caller.first.split(':', 2)
|
27
|
+
line = line.to_i
|
28
|
+
|
29
|
+
methods.each do |method|
|
30
|
+
name = ::Agrippa::Methods.name(method, options)
|
31
|
+
generated = ::Agrippa::Delegation.interpolate(definition,
|
32
|
+
name: name, target: target, method: method)
|
33
|
+
base.send(:module_eval, generated, file, line)
|
34
|
+
end
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
module ClassMethods
|
39
|
+
def delegate(*methods)
|
40
|
+
::Agrippa::Delegation.build(self, caller, methods, <<-END)
|
41
|
+
def NAME(*args, &block)
|
42
|
+
TARGET.METHOD(*args, &block)
|
43
|
+
end
|
44
|
+
END
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
def class_delegate(*methods)
|
49
|
+
::Agrippa::Delegation.build(self, caller, methods, <<-END)
|
50
|
+
def self.NAME(*args, &block)
|
51
|
+
TARGET.METHOD(*args, &block)
|
52
|
+
end
|
53
|
+
END
|
54
|
+
self
|
55
|
+
end
|
56
|
+
|
57
|
+
def delegate_command(*methods)
|
58
|
+
::Agrippa::Delegation.build(self, caller, methods, <<-END)
|
59
|
+
def NAME(*args, &block)
|
60
|
+
store('TARGET', TARGET.METHOD(*args, &block))
|
61
|
+
end
|
62
|
+
END
|
63
|
+
self
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require "agrippa/utilities"
|
2
|
+
require "agrippa/methods"
|
3
|
+
require "agrippa/accessor_methods"
|
4
|
+
|
5
|
+
Agrippa::Utilities.try_require("hamster/hash",
|
6
|
+
"Agrippa::Immutable requires the Hamster gem.")
|
7
|
+
|
8
|
+
module Agrippa
|
9
|
+
module Immutable
|
10
|
+
def self.included(base)
|
11
|
+
base.send(:include, Agrippa::Methods)
|
12
|
+
return if base.respond_to?(:state_reader)
|
13
|
+
base.send(:extend, ClassMethods)
|
14
|
+
base.send(:include, InstanceMethods)
|
15
|
+
base.send(:mark_as_commands, :chain, :store)
|
16
|
+
base.send(:private, :__symbolize_keys)
|
17
|
+
end
|
18
|
+
|
19
|
+
module ClassMethods
|
20
|
+
include AccessorMethods
|
21
|
+
|
22
|
+
def __define_state_reader(key, original_caller, options)
|
23
|
+
name = ::Agrippa::Methods.name(key, options)
|
24
|
+
file, line = original_caller.first.split(':', 2)
|
25
|
+
line = line.to_i
|
26
|
+
spec = "def #{name}; fetch(:#{key}); end"
|
27
|
+
module_eval(spec, file, line)
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
def __define_state_writer(key, original_caller, options)
|
32
|
+
name = ::Agrippa::Methods.name(key, options)
|
33
|
+
name = ::Agrippa::Methods.name(name, prefix: "set") \
|
34
|
+
unless (options[:prefix] == false)
|
35
|
+
file, line = original_caller.first.split(':', 2)
|
36
|
+
line = line.to_i
|
37
|
+
spec = "def #{name}(v); store(:#{key}, v); end"
|
38
|
+
module_eval(spec, file, line)
|
39
|
+
mark_as_command(name)
|
40
|
+
self
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
module InstanceMethods
|
45
|
+
def initialize(state = Hamster::Hash.new, apply_default = true)
|
46
|
+
raise(ArgumentError, "#{self.class}#new requires a hash.") \
|
47
|
+
unless state.respond_to?(:each_pair)
|
48
|
+
if(apply_default and respond_to?(:default_state))
|
49
|
+
@state = Hamster::Hash.new(default_state)
|
50
|
+
@state = @state.merge(state) unless state.nil?
|
51
|
+
elsif(state.is_a?(Hamster::Hash))
|
52
|
+
@state = state
|
53
|
+
else
|
54
|
+
@state = Hamster::Hash.new(__symbolize_keys(state))
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def chain(updates)
|
59
|
+
raise(ArgementError, "#set requires a Hash") \
|
60
|
+
unless updates.respond_to?(:each_pair)
|
61
|
+
self.class.new(@state.merge(__symbolize_keys(updates)), false)
|
62
|
+
end
|
63
|
+
|
64
|
+
def store(key, value)
|
65
|
+
self.class.new(@state.store(key.to_sym, value), false)
|
66
|
+
end
|
67
|
+
|
68
|
+
def fetch(key, default = nil)
|
69
|
+
@state.fetch(key.to_sym, default)
|
70
|
+
end
|
71
|
+
|
72
|
+
def __symbolize_keys(input)
|
73
|
+
output = input.dup
|
74
|
+
input.keys { |k| output[k.to_sym] = output.delete(k) \
|
75
|
+
unless k.is_a?(Symbol) }
|
76
|
+
output
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require "agrippa/proxy"
|
2
|
+
|
3
|
+
module Agrippa
|
4
|
+
class Maybe < Proxy
|
5
|
+
def method_missing(method, *args, &block)
|
6
|
+
return(self) if @value.nil?
|
7
|
+
output = begin
|
8
|
+
@value.send(method, *args, &block)
|
9
|
+
rescue ::NoMethodError
|
10
|
+
nil
|
11
|
+
end
|
12
|
+
return(output) unless proxied_method?(method)
|
13
|
+
@value = output
|
14
|
+
__chain(@value)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Agrippa
|
2
|
+
class MethodRedefinitionError < StandardError
|
3
|
+
def initialize(klass, method)
|
4
|
+
@klass, @method = klass, method
|
5
|
+
end
|
6
|
+
|
7
|
+
def to_s
|
8
|
+
"Redefinition of #{@klass}##{@method}; use the 'redefine' option if you really want to overwrite the existing method."
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module Methods
|
13
|
+
def self.included(base)
|
14
|
+
return if base.respond_to?(:command_methods)
|
15
|
+
base.send(:extend, ClassMethods)
|
16
|
+
base.send(:include, InstanceMethods)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.name(name, options = {})
|
20
|
+
result = name
|
21
|
+
suffix = options[:suffix]
|
22
|
+
result = "#{result}_#{suffix}".to_sym if suffix
|
23
|
+
prefix = options[:prefix]
|
24
|
+
result = "#{prefix}_#{result}".to_sym if prefix
|
25
|
+
result
|
26
|
+
end
|
27
|
+
|
28
|
+
module ClassMethods
|
29
|
+
def mark_as_commands(*args)
|
30
|
+
args.flatten.each do |arg|
|
31
|
+
command_methods[arg.to_sym] = true
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
alias_method :mark_as_command, :mark_as_commands
|
36
|
+
|
37
|
+
def command_methods
|
38
|
+
@__command_methods ||= {}
|
39
|
+
end
|
40
|
+
|
41
|
+
def command_method?(name)
|
42
|
+
command_methods.has_key?(name.to_sym)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
module InstanceMethods
|
47
|
+
def command_methods
|
48
|
+
self.class.command_methods
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require "agrippa/methods"
|
2
|
+
require "agrippa/accessor_methods"
|
3
|
+
|
4
|
+
module Agrippa
|
5
|
+
module Mutable
|
6
|
+
def self.included(base)
|
7
|
+
base.send(:include, Agrippa::Methods)
|
8
|
+
return if base.respond_to?(:state_reader)
|
9
|
+
base.send(:extend, ClassMethods)
|
10
|
+
base.send(:include, InstanceMethods)
|
11
|
+
base.send(:mark_as_commands, :chain, :store)
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
include AccessorMethods
|
16
|
+
|
17
|
+
def __define_state_reader(key, original_caller, options)
|
18
|
+
name = ::Agrippa::Methods.name(key, options)
|
19
|
+
file, line = original_caller.first.split(':', 2)
|
20
|
+
line = line.to_i
|
21
|
+
spec = "def #{name}; @#{key}; end"
|
22
|
+
module_eval(spec, file, line)
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
def __define_state_writer(key, original_caller, options)
|
27
|
+
name = ::Agrippa::Methods.name(key, options)
|
28
|
+
name = ::Agrippa::Methods.name(name, prefix: "set") \
|
29
|
+
unless (options[:prefix] == false)
|
30
|
+
file, line = original_caller.first.split(':', 2)
|
31
|
+
line = line.to_i
|
32
|
+
spec = "def #{name}(value); @#{key} = value; self; end"
|
33
|
+
module_eval(spec, file, line)
|
34
|
+
mark_as_command(name)
|
35
|
+
self
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
module InstanceMethods
|
40
|
+
def initialize(state = nil)
|
41
|
+
chain(default_state) if respond_to?(:default_state)
|
42
|
+
chain(state) unless state.nil?
|
43
|
+
end
|
44
|
+
|
45
|
+
def chain(state)
|
46
|
+
raise(ArgumentError, "#set requires a Hash") \
|
47
|
+
unless state.respond_to?(:each_pair)
|
48
|
+
state.each_pair { |key, value| store(key, value) }
|
49
|
+
self
|
50
|
+
end
|
51
|
+
|
52
|
+
def store(key, value)
|
53
|
+
instance_variable_set("@#{key}", value)
|
54
|
+
self
|
55
|
+
end
|
56
|
+
|
57
|
+
def fetch(key, default = nil)
|
58
|
+
instance_variable_get("@#{key}") || default
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require "agrippa/methods"
|
2
|
+
require "agrippa/accessor_methods"
|
3
|
+
|
4
|
+
module Agrippa
|
5
|
+
module MutableHash
|
6
|
+
def self.included(base)
|
7
|
+
base.send(:include, Agrippa::Methods)
|
8
|
+
return if base.respond_to?(:state_reader)
|
9
|
+
base.send(:extend, ClassMethods)
|
10
|
+
base.send(:include, InstanceMethods)
|
11
|
+
base.send(:mark_as_commands, :chain, :store)
|
12
|
+
base.send(:private, :__apply_default_state)
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
include AccessorMethods
|
17
|
+
|
18
|
+
def __define_state_reader(key, original_caller, options)
|
19
|
+
name = ::Agrippa::Methods.name(key, options)
|
20
|
+
file, line = original_caller.first.split(':', 2)
|
21
|
+
line = line.to_i
|
22
|
+
spec = "def #{name}; @state[:#{key}]; end"
|
23
|
+
module_eval(spec, file, line)
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def __define_state_writer(key, original_caller, options)
|
28
|
+
name = ::Agrippa::Methods.name(key, options)
|
29
|
+
name = ::Agrippa::Methods.name(name, prefix: "set") \
|
30
|
+
unless (options[:prefix] == false)
|
31
|
+
file, line = original_caller.first.split(':', 2)
|
32
|
+
line = line.to_i
|
33
|
+
spec = "def #{name}(v); @state[:#{key}] = v; self; end"
|
34
|
+
module_eval(spec, file, line)
|
35
|
+
mark_as_command(name)
|
36
|
+
self
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
module InstanceMethods
|
41
|
+
def initialize(state = nil)
|
42
|
+
raise(ArgumentError, "#{self.class}#new requires a hash.") \
|
43
|
+
unless (state.nil? or state.respond_to?(:each_pair))
|
44
|
+
__apply_default_state
|
45
|
+
chain(state) if (state.respond_to?(:each_pair))
|
46
|
+
end
|
47
|
+
|
48
|
+
def chain(updates)
|
49
|
+
raise(ArgumentError, "#set requires a Hash") \
|
50
|
+
unless updates.respond_to?(:each_pair)
|
51
|
+
updates.each_pair { |key, value| store(key, value) }
|
52
|
+
self
|
53
|
+
end
|
54
|
+
|
55
|
+
def store(key, value)
|
56
|
+
@state.store(key.to_sym, value)
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
def fetch(key, default = nil)
|
61
|
+
@state.fetch(key.to_sym, default)
|
62
|
+
end
|
63
|
+
|
64
|
+
def __apply_default_state
|
65
|
+
return(self) unless @state.nil?
|
66
|
+
@state = default_state if respond_to?(:default_state)
|
67
|
+
@state ||= {}
|
68
|
+
self
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require "agrippa/methods"
|
2
|
+
|
3
|
+
module Agrippa
|
4
|
+
class Proxy < BasicObject
|
5
|
+
include Methods
|
6
|
+
|
7
|
+
instance_methods.each do |method|
|
8
|
+
next if (method =~ /(^__|^nil\?$|^send$|^object_id$)/)
|
9
|
+
undef_method(method)
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :proxied_methods
|
13
|
+
|
14
|
+
def initialize(value, *methods)
|
15
|
+
@value, @proxied_methods = value, {}
|
16
|
+
__build_method_lookup_table(value, methods.flatten)
|
17
|
+
end
|
18
|
+
|
19
|
+
def _value
|
20
|
+
@value
|
21
|
+
end
|
22
|
+
|
23
|
+
def _
|
24
|
+
@value
|
25
|
+
end
|
26
|
+
|
27
|
+
def _deep_value
|
28
|
+
return(@value) unless @value.respond_to?(:_value)
|
29
|
+
@value._value
|
30
|
+
end
|
31
|
+
|
32
|
+
def respond_to?(method, include_private = false)
|
33
|
+
return(true) if (method == :_value)
|
34
|
+
return(true) if (method == :proxied_methods)
|
35
|
+
return(true) if (method == :proxied_method?)
|
36
|
+
@value.respond_to?(method, include_private)
|
37
|
+
end
|
38
|
+
|
39
|
+
def is_a?(klass)
|
40
|
+
@value.is_a?(klass) || (klass == ::Agrippa::Proxy)
|
41
|
+
end
|
42
|
+
|
43
|
+
def proxied_method?(method)
|
44
|
+
@proxied_methods.empty? \
|
45
|
+
or @proxied_methods.has_key?(method.to_sym)
|
46
|
+
end
|
47
|
+
|
48
|
+
def method_missing(method, *args, &block)
|
49
|
+
::Kernel.raise(::NoMethodError,
|
50
|
+
"Implement method_missing in a subclass.")
|
51
|
+
end
|
52
|
+
|
53
|
+
def __set_proxied_methods(lookup_hash)
|
54
|
+
@proxied_methods = lookup_hash
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def __class
|
61
|
+
@__class ||= (class << self; self end).superclass
|
62
|
+
end
|
63
|
+
|
64
|
+
def __chain(value)
|
65
|
+
__class.new(value, false).__set_proxied_methods(@proxied_methods)
|
66
|
+
end
|
67
|
+
|
68
|
+
def __build_method_lookup_table(value, methods)
|
69
|
+
return(self) if (methods.first == false)
|
70
|
+
@proxied_methods.merge!(value.command_methods) \
|
71
|
+
if value.respond_to?(:command_methods)
|
72
|
+
methods.each { |method| @proxied_methods[method.to_sym] = true }
|
73
|
+
self
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|