rjspotter-innate 2009.06.29
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.
- data/AUTHORS +10 -0
- data/CHANGELOG +3261 -0
- data/COPYING +18 -0
- data/MANIFEST +127 -0
- data/README.md +563 -0
- data/Rakefile +39 -0
- data/example/app/retro_games.rb +60 -0
- data/example/app/todo/layout/default.xhtml +11 -0
- data/example/app/todo/spec/todo.rb +63 -0
- data/example/app/todo/start.rb +51 -0
- data/example/app/todo/view/index.xhtml +39 -0
- data/example/app/whywiki_erb/layout/wiki.html.erb +15 -0
- data/example/app/whywiki_erb/spec/wiki.rb +19 -0
- data/example/app/whywiki_erb/start.rb +42 -0
- data/example/app/whywiki_erb/view/edit.erb +6 -0
- data/example/app/whywiki_erb/view/index.erb +12 -0
- data/example/custom_middleware.rb +35 -0
- data/example/hello.rb +11 -0
- data/example/howto_spec.rb +35 -0
- data/example/link.rb +27 -0
- data/example/provides.rb +31 -0
- data/example/session.rb +38 -0
- data/innate.gemspec +41 -0
- data/lib/innate.rb +269 -0
- data/lib/innate/action.rb +137 -0
- data/lib/innate/adapter.rb +76 -0
- data/lib/innate/cache.rb +134 -0
- data/lib/innate/cache/api.rb +128 -0
- data/lib/innate/cache/drb.rb +58 -0
- data/lib/innate/cache/file_based.rb +44 -0
- data/lib/innate/cache/marshal.rb +20 -0
- data/lib/innate/cache/memory.rb +21 -0
- data/lib/innate/cache/yaml.rb +20 -0
- data/lib/innate/current.rb +35 -0
- data/lib/innate/dynamap.rb +96 -0
- data/lib/innate/helper.rb +185 -0
- data/lib/innate/helper/aspect.rb +124 -0
- data/lib/innate/helper/cgi.rb +54 -0
- data/lib/innate/helper/flash.rb +36 -0
- data/lib/innate/helper/link.rb +94 -0
- data/lib/innate/helper/redirect.rb +85 -0
- data/lib/innate/helper/render.rb +152 -0
- data/lib/innate/helper/send_file.rb +26 -0
- data/lib/innate/log.rb +20 -0
- data/lib/innate/log/color_formatter.rb +49 -0
- data/lib/innate/log/hub.rb +77 -0
- data/lib/innate/middleware_compiler.rb +65 -0
- data/lib/innate/mock.rb +49 -0
- data/lib/innate/node.rb +1029 -0
- data/lib/innate/options.rb +37 -0
- data/lib/innate/options/dsl.rb +205 -0
- data/lib/innate/options/stub.rb +7 -0
- data/lib/innate/request.rb +141 -0
- data/lib/innate/response.rb +24 -0
- data/lib/innate/route.rb +114 -0
- data/lib/innate/session.rb +133 -0
- data/lib/innate/session/flash.rb +94 -0
- data/lib/innate/spec.rb +1 -0
- data/lib/innate/spec/bacon.rb +28 -0
- data/lib/innate/state.rb +26 -0
- data/lib/innate/state/accessor.rb +130 -0
- data/lib/innate/traited.rb +90 -0
- data/lib/innate/trinity.rb +18 -0
- data/lib/innate/version.rb +3 -0
- data/lib/innate/view.rb +97 -0
- data/lib/innate/view/erb.rb +14 -0
- data/lib/innate/view/etanni.rb +33 -0
- data/lib/innate/view/none.rb +9 -0
- data/spec/example/app/retro_games.rb +30 -0
- data/spec/example/hello.rb +13 -0
- data/spec/example/link.rb +25 -0
- data/spec/example/provides.rb +16 -0
- data/spec/example/session.rb +22 -0
- data/spec/helper.rb +10 -0
- data/spec/innate/action/layout.rb +121 -0
- data/spec/innate/action/layout/file_layout.xhtml +1 -0
- data/spec/innate/cache/common.rb +47 -0
- data/spec/innate/cache/marshal.rb +5 -0
- data/spec/innate/cache/memory.rb +5 -0
- data/spec/innate/cache/yaml.rb +5 -0
- data/spec/innate/dynamap.rb +22 -0
- data/spec/innate/helper.rb +86 -0
- data/spec/innate/helper/aspect.rb +75 -0
- data/spec/innate/helper/cgi.rb +37 -0
- data/spec/innate/helper/flash.rb +115 -0
- data/spec/innate/helper/link.rb +139 -0
- data/spec/innate/helper/redirect.rb +171 -0
- data/spec/innate/helper/render.rb +165 -0
- data/spec/innate/helper/send_file.rb +21 -0
- data/spec/innate/helper/view/aspect_hello.xhtml +1 -0
- data/spec/innate/helper/view/locals.xhtml +1 -0
- data/spec/innate/helper/view/loop.xhtml +4 -0
- data/spec/innate/helper/view/num.xhtml +1 -0
- data/spec/innate/helper/view/partial.xhtml +1 -0
- data/spec/innate/helper/view/recursive.xhtml +7 -0
- data/spec/innate/mock.rb +84 -0
- data/spec/innate/modes.rb +61 -0
- data/spec/innate/node/mapping.rb +37 -0
- data/spec/innate/node/node.rb +135 -0
- data/spec/innate/node/resolve.rb +82 -0
- data/spec/innate/node/view/another_layout/another_layout.xhtml +3 -0
- data/spec/innate/node/view/bar.xhtml +1 -0
- data/spec/innate/node/view/foo.html.xhtml +1 -0
- data/spec/innate/node/view/only_view.xhtml +1 -0
- data/spec/innate/node/view/with_layout.xhtml +1 -0
- data/spec/innate/node/wrap_action_call.rb +83 -0
- data/spec/innate/options.rb +123 -0
- data/spec/innate/parameter.rb +154 -0
- data/spec/innate/provides.rb +99 -0
- data/spec/innate/provides/list.html.xhtml +1 -0
- data/spec/innate/provides/list.txt.xhtml +1 -0
- data/spec/innate/request.rb +79 -0
- data/spec/innate/route.rb +135 -0
- data/spec/innate/session.rb +58 -0
- data/spec/innate/traited.rb +55 -0
- data/tasks/authors.rake +30 -0
- data/tasks/bacon.rake +66 -0
- data/tasks/changelog.rake +18 -0
- data/tasks/gem.rake +22 -0
- data/tasks/gem_setup.rake +99 -0
- data/tasks/grancher.rake +12 -0
- data/tasks/manifest.rake +4 -0
- data/tasks/rcov.rake +19 -0
- data/tasks/release.rake +53 -0
- data/tasks/reversion.rake +8 -0
- data/tasks/setup.rake +6 -0
- data/tasks/ycov.rake +84 -0
- metadata +218 -0
@@ -0,0 +1,133 @@
|
|
1
|
+
module Innate
|
2
|
+
|
3
|
+
# Mostly ported from Ramaze, but behaves lazy, no session will be created if
|
4
|
+
# no session is used.
|
5
|
+
#
|
6
|
+
# We keep session data in memory until #flush is called, at which point it
|
7
|
+
# will be persisted completely into the cache, no question asked.
|
8
|
+
#
|
9
|
+
# You may store anything in here that you may also store in the corresponding
|
10
|
+
# store, usually it's best to keep it to things that are safe to Marshal.
|
11
|
+
#
|
12
|
+
# The Session instance is compatible with the specification of rack.session.
|
13
|
+
#
|
14
|
+
# Since the Time class is used to create the cookie expiration timestamp, you
|
15
|
+
# will have to keep the ttl in a reasonable range.
|
16
|
+
# The maximum value that Time can store on a 32bit system is:
|
17
|
+
# Time.at(2147483647) # => Tue Jan 19 12:14:07 +0900 2038
|
18
|
+
#
|
19
|
+
# The default expiration time for cookies and the session cache was reduced
|
20
|
+
# to a default of 30 days.
|
21
|
+
# This was done to be compatible with the maximum ttl of MemCache. You may
|
22
|
+
# increase this value if you do not use MemCache to persist your sessions.
|
23
|
+
class Session
|
24
|
+
include Optioned
|
25
|
+
|
26
|
+
options.dsl do
|
27
|
+
o "Key for the session cookie",
|
28
|
+
:key, 'innate.sid'
|
29
|
+
o "Domain the cookie relates to, unspecified if false",
|
30
|
+
:domain, false
|
31
|
+
o "Path the cookie relates to",
|
32
|
+
:path, '/'
|
33
|
+
o "Use secure cookie",
|
34
|
+
:secure, false
|
35
|
+
o "Time of cookie expiration",
|
36
|
+
:expires, nil
|
37
|
+
o "Time to live for session cookies and cache, nil/false will prevent setting",
|
38
|
+
:ttl, (60 * 60 * 24 * 30) # 30 days
|
39
|
+
|
40
|
+
trigger(:expires){|v|
|
41
|
+
self.ttl = v - Time.now.to_i
|
42
|
+
Log.warn("Innate::Session.options.expires is deprecated, use #ttl instead")
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
attr_reader :cookie_set, :request, :response, :flash
|
47
|
+
|
48
|
+
def initialize(request, response)
|
49
|
+
@request, @response = request, response
|
50
|
+
@cookie_set = false
|
51
|
+
@cache_sid = nil
|
52
|
+
@flash = Flash.new(self)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Rack interface
|
56
|
+
|
57
|
+
def store(key, value)
|
58
|
+
cache_sid[key] = value
|
59
|
+
end
|
60
|
+
alias []= store
|
61
|
+
|
62
|
+
def fetch(key, value = nil)
|
63
|
+
cache_sid[key]
|
64
|
+
end
|
65
|
+
alias [] fetch
|
66
|
+
|
67
|
+
def delete(key)
|
68
|
+
cache_sid.delete(key)
|
69
|
+
end
|
70
|
+
|
71
|
+
def clear
|
72
|
+
cache.delete(sid)
|
73
|
+
@cache_sid = nil
|
74
|
+
end
|
75
|
+
|
76
|
+
# Additional interface
|
77
|
+
|
78
|
+
def flush(response = @response)
|
79
|
+
return if !@cache_sid or @cache_sid.empty?
|
80
|
+
|
81
|
+
flash.rotate!
|
82
|
+
cache.store(sid, cache_sid, :ttl => options.ttl)
|
83
|
+
set_cookie(response)
|
84
|
+
end
|
85
|
+
|
86
|
+
def sid
|
87
|
+
@sid ||= cookie || generate_sid
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def cache_sid
|
93
|
+
@cache_sid ||= cache[sid] || {}
|
94
|
+
end
|
95
|
+
|
96
|
+
def cookie
|
97
|
+
@request.cookies[options.key]
|
98
|
+
end
|
99
|
+
|
100
|
+
def cache
|
101
|
+
Innate::Cache.session
|
102
|
+
end
|
103
|
+
|
104
|
+
def set_cookie(response)
|
105
|
+
return if @cookie_set || cookie
|
106
|
+
|
107
|
+
@cookie_set = true
|
108
|
+
response.set_cookie(options.key, cookie_value)
|
109
|
+
end
|
110
|
+
|
111
|
+
def cookie_value
|
112
|
+
o = options
|
113
|
+
cookie = {:domain => o.domain, :path => o.path, :secure => o.secure}
|
114
|
+
cookie[:expires] = (Time.now + o.ttl) if o.ttl
|
115
|
+
cookie.merge!(:value => sid)
|
116
|
+
end
|
117
|
+
|
118
|
+
def generate_sid
|
119
|
+
begin sid = sid_algorithm end while cache[sid]
|
120
|
+
sid
|
121
|
+
end
|
122
|
+
|
123
|
+
begin
|
124
|
+
require 'securerandom'
|
125
|
+
def sid_algorithm; SecureRandom.hex(32); end
|
126
|
+
rescue LoadError
|
127
|
+
def sid_algorithm
|
128
|
+
entropy = [ srand, rand, Time.now.to_f, rand, $$, rand, object_id ]
|
129
|
+
Digest::SHA2.hexdigest(entropy.join)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module Innate
|
2
|
+
class Session
|
3
|
+
|
4
|
+
# The purpose of this class is to act as a unifier of the previous
|
5
|
+
# and current flash.
|
6
|
+
#
|
7
|
+
# Flash means pairs of keys and values that are held only over one
|
8
|
+
# request/response cycle. So you can assign a key/value in the current
|
9
|
+
# session and retrieve it in the current and following request.
|
10
|
+
#
|
11
|
+
# Please see the Innate::Helper::Flash for details on the usage in your
|
12
|
+
# application.
|
13
|
+
class Flash
|
14
|
+
include Enumerable
|
15
|
+
|
16
|
+
def initialize(session)
|
17
|
+
@session = session
|
18
|
+
end
|
19
|
+
|
20
|
+
# iterate over the combined session
|
21
|
+
def each(&block)
|
22
|
+
combined.each(&block)
|
23
|
+
end
|
24
|
+
|
25
|
+
# the current session[:FLASH_PREVIOUS]
|
26
|
+
def previous
|
27
|
+
session[:FLASH_PREVIOUS] || {}
|
28
|
+
end
|
29
|
+
|
30
|
+
# the current session[:FLASH]
|
31
|
+
def current
|
32
|
+
session[:FLASH] ||= {}
|
33
|
+
end
|
34
|
+
|
35
|
+
# combined key/value pairs of previous and current
|
36
|
+
# current keys overshadow the old ones.
|
37
|
+
def combined
|
38
|
+
previous.merge(current)
|
39
|
+
end
|
40
|
+
|
41
|
+
# flash[key] in your Controller
|
42
|
+
def [](key)
|
43
|
+
combined[key]
|
44
|
+
end
|
45
|
+
|
46
|
+
# flash[key] = value in your Controller
|
47
|
+
def []=(key, value)
|
48
|
+
prev = session[:FLASH] || {}
|
49
|
+
prev[key] = value
|
50
|
+
session[:FLASH] = prev
|
51
|
+
end
|
52
|
+
|
53
|
+
# Inspects combined
|
54
|
+
def inspect
|
55
|
+
combined.inspect
|
56
|
+
end
|
57
|
+
|
58
|
+
# Delete a key
|
59
|
+
def delete(key)
|
60
|
+
previous.delete(key)
|
61
|
+
current.delete(key)
|
62
|
+
end
|
63
|
+
|
64
|
+
# check if combined is empty
|
65
|
+
def empty?
|
66
|
+
combined.empty?
|
67
|
+
end
|
68
|
+
|
69
|
+
# merge into current
|
70
|
+
def merge!(hash)
|
71
|
+
current.merge!(hash)
|
72
|
+
end
|
73
|
+
|
74
|
+
# merge on current
|
75
|
+
def merge(hash)
|
76
|
+
current.merge(hash)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Rotation means current values are assigned as old values for the next
|
80
|
+
# request.
|
81
|
+
def rotate!
|
82
|
+
old = session.delete(:FLASH)
|
83
|
+
session[:FLASH_PREVIOUS] = old if old
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
# Associated session object
|
89
|
+
def session
|
90
|
+
@session
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
data/lib/innate/spec.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'innate/spec/bacon'
|
@@ -0,0 +1,28 @@
|
|
1
|
+
begin; require 'rubygems'; rescue LoadError; end
|
2
|
+
|
3
|
+
require 'bacon'
|
4
|
+
require 'rack/test'
|
5
|
+
require File.expand_path('../../', __FILE__) unless defined?(Innate)
|
6
|
+
|
7
|
+
Bacon.summary_on_exit
|
8
|
+
|
9
|
+
module Innate
|
10
|
+
# minimal middleware, no exception handling
|
11
|
+
middleware(:spec){|m| m.innate }
|
12
|
+
|
13
|
+
# skip starting adapter
|
14
|
+
options.started = true
|
15
|
+
options.mode = :spec
|
16
|
+
end
|
17
|
+
|
18
|
+
shared :rack_test do
|
19
|
+
Innate.setup_dependencies
|
20
|
+
extend Rack::Test::Methods
|
21
|
+
|
22
|
+
def app; Innate.middleware; end
|
23
|
+
end
|
24
|
+
|
25
|
+
shared :mock do
|
26
|
+
warn 'behaves_like(:mock) is deprecated, use behaves_like(:rack_test) instead'
|
27
|
+
behaves_like :rack_test
|
28
|
+
end
|
data/lib/innate/state.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module Innate
|
4
|
+
module SingletonMethods
|
5
|
+
# Use this method to achieve thread-safety for sensitive operations.
|
6
|
+
#
|
7
|
+
# This should be of most use when manipulating files to prevent other
|
8
|
+
# threads from doing the same, no other code will be scheduled during
|
9
|
+
# execution of this method.
|
10
|
+
#
|
11
|
+
# @param [Proc] block the things you want to execute
|
12
|
+
# @see State::Thread#sync State::Fiber#sync
|
13
|
+
def sync(&block)
|
14
|
+
Thread.exclusive(&block)
|
15
|
+
end
|
16
|
+
|
17
|
+
def defer
|
18
|
+
outer = ::Thread.current
|
19
|
+
::Thread.new{
|
20
|
+
inner = ::Thread.current
|
21
|
+
outer.keys.each{|k| inner[k] = outer[k] }
|
22
|
+
yield
|
23
|
+
}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
module Innate
|
2
|
+
# Simplify accessing Thread.current variables.
|
3
|
+
#
|
4
|
+
# Example:
|
5
|
+
#
|
6
|
+
# class Foo
|
7
|
+
# include Innate::StateAccessor
|
8
|
+
# state_accessor :session
|
9
|
+
#
|
10
|
+
# def calculate
|
11
|
+
# session[:num1] + session[:num2]
|
12
|
+
# end
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# Foo#calculate can now be called from anywhere in your application and it
|
16
|
+
# will have direct access to the session in the current request/response
|
17
|
+
# cycle in a thread-safe way without the need to explicitly pass the session
|
18
|
+
# along.
|
19
|
+
|
20
|
+
module StateAccessor
|
21
|
+
|
22
|
+
# Iterate over the names and yield accordingly.
|
23
|
+
# names are either objects responding to #to_sym or hashes.
|
24
|
+
#
|
25
|
+
# It's only used within this module to make the code readable.
|
26
|
+
#
|
27
|
+
# Used below.
|
28
|
+
|
29
|
+
def self.each(*names)
|
30
|
+
names.each do |name|
|
31
|
+
if name.respond_to?(:to_hash)
|
32
|
+
name.to_hash.each do |key, meth|
|
33
|
+
yield(key.to_sym, meth.to_sym)
|
34
|
+
end
|
35
|
+
else
|
36
|
+
key = meth = name.to_sym
|
37
|
+
yield(key, meth)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Combined state_writer and state_reader.
|
43
|
+
# +initializer+ is a block that may be given and its result will be the new
|
44
|
+
# value in case the method created by state_reader was never called before
|
45
|
+
# and the value wasn't set before.
|
46
|
+
#
|
47
|
+
# Example:
|
48
|
+
#
|
49
|
+
# state_accessor(:session)
|
50
|
+
# state_accessor(:user){ session[:user] }
|
51
|
+
|
52
|
+
def state_accessor(*names, &initializer)
|
53
|
+
state_writer(*names)
|
54
|
+
state_reader(*names, &initializer)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Writer accessor to Thread.current[key]=
|
58
|
+
#
|
59
|
+
# Example:
|
60
|
+
#
|
61
|
+
# class Foo
|
62
|
+
# include Innate::StateAccessor
|
63
|
+
# state_writer(:result)
|
64
|
+
#
|
65
|
+
# def calculate
|
66
|
+
# self.result = 42
|
67
|
+
# end
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
# class Bar
|
71
|
+
# include Innate::StateAccessor
|
72
|
+
# state_reader(:result)
|
73
|
+
#
|
74
|
+
# def calculcate
|
75
|
+
# result * result
|
76
|
+
# end
|
77
|
+
# end
|
78
|
+
#
|
79
|
+
# Foo.new.calculate # => 42
|
80
|
+
# Bar.new.calculate # => 1764
|
81
|
+
|
82
|
+
def state_writer(*names)
|
83
|
+
StateAccessor.each(*names) do |key, meth|
|
84
|
+
class_eval("def %s=(obj) Thread.current[%p] = obj; end" % [meth, key])
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Reader accessor for Thread.current[key]
|
89
|
+
#
|
90
|
+
# Example:
|
91
|
+
#
|
92
|
+
# class Foo
|
93
|
+
# include Innate::StateAccessor
|
94
|
+
# state_reader(:session)
|
95
|
+
# state_reader(:random){ rand(100_000) }
|
96
|
+
#
|
97
|
+
# def calculate
|
98
|
+
# val1 = session[:num1] + session[:num2] + random
|
99
|
+
# val2 = session[:num1] + session[:num2] + random
|
100
|
+
# val1 == val2 # => true
|
101
|
+
# end
|
102
|
+
# end
|
103
|
+
#
|
104
|
+
# NOTE:
|
105
|
+
# If given +initializer+, there will be some performance impact since we
|
106
|
+
# cannot use class_eval and have to use define_method instead, we also
|
107
|
+
# have to check every time whether the initializer was executed already.
|
108
|
+
#
|
109
|
+
# In 1.8.x the overhead of define_method is 3x that of class_eval/def
|
110
|
+
# In 1.9.1 the overhead of define_method is 1.5x that of class_eval/def
|
111
|
+
#
|
112
|
+
# This may only be an issue for readers that are called a lot of times.
|
113
|
+
|
114
|
+
def state_reader(*names, &initializer)
|
115
|
+
StateAccessor.each(*names) do |key, meth|
|
116
|
+
if initializer
|
117
|
+
define_method(meth) do
|
118
|
+
unless Thread.current.key?(key)
|
119
|
+
Thread.current[key] = instance_eval(&initializer)
|
120
|
+
else
|
121
|
+
Thread.current[key]
|
122
|
+
end
|
123
|
+
end
|
124
|
+
else
|
125
|
+
class_eval("def %s; Thread.current[%p]; end" % [meth, key])
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Innate
|
2
|
+
# Traited helps you doing configuration similar to class variables.
|
3
|
+
#
|
4
|
+
# It's built on a simple Hash, where keys are objects and the values the
|
5
|
+
# configuration.
|
6
|
+
# By using {Traited#ancestral_trait} you will get nicely inherited
|
7
|
+
# configuration, where keys later in the ancestors will take precedence.
|
8
|
+
#
|
9
|
+
# @example usage
|
10
|
+
#
|
11
|
+
# class Foo
|
12
|
+
# include Innate::Traited
|
13
|
+
# trait :hello => 'Hello'
|
14
|
+
#
|
15
|
+
# def initialize
|
16
|
+
# trait :hello => 'World!'
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# def show
|
20
|
+
# [class_trait[:hello], trait[:hello], ancestral_trait[:hello]]
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# Foo.trait[:hello] # => "Hello"
|
25
|
+
# foo = Foo.new
|
26
|
+
# foo.trait[:hello] # => "World!"
|
27
|
+
# foo.show # => ["Hello", "World!", "World!"]
|
28
|
+
module Traited
|
29
|
+
TRAITS, ANCESTRAL_TRAITS, ANCESTRAL_VALUES = {}, {}, {}
|
30
|
+
|
31
|
+
def self.included(into)
|
32
|
+
into.extend(self)
|
33
|
+
end
|
34
|
+
|
35
|
+
def trait(hash = nil)
|
36
|
+
if hash
|
37
|
+
TRAITS[self] ||= {}
|
38
|
+
result = TRAITS[self].merge!(hash)
|
39
|
+
ANCESTRAL_VALUES.clear
|
40
|
+
ANCESTRAL_TRAITS.clear
|
41
|
+
result
|
42
|
+
else
|
43
|
+
TRAITS[self] || {}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Builds a trait from all the ancestors, closer ancestors overwrite distant
|
48
|
+
# ancestors
|
49
|
+
#
|
50
|
+
# class Foo
|
51
|
+
# include Innate::Traited
|
52
|
+
# trait :one => :eins, :first => :erstes
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
# class Bar < Foo
|
56
|
+
# trait :two => :zwei
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# class Foobar < Bar
|
60
|
+
# trait :three => :drei, :first => :overwritten
|
61
|
+
# end
|
62
|
+
#
|
63
|
+
# Foobar.ancestral_trait
|
64
|
+
# # => {:three => :drei, :two => :zwei, :one => :eins, :first => :overwritten}
|
65
|
+
def ancestral_trait
|
66
|
+
klass = self.kind_of?(Module) ? self : self.class
|
67
|
+
ANCESTRAL_TRAITS[klass] ||=
|
68
|
+
each_ancestral_trait({}){|hash, trait| hash.update(trait) }
|
69
|
+
end
|
70
|
+
|
71
|
+
def ancestral_trait_values(key)
|
72
|
+
klass = self.kind_of?(Module) ? self : self.class
|
73
|
+
cache = ANCESTRAL_VALUES[klass] ||= {}
|
74
|
+
cache[key] ||= each_ancestral_trait([]){|array, trait|
|
75
|
+
array << trait[key] if trait.key?(key) }
|
76
|
+
end
|
77
|
+
|
78
|
+
def each_ancestral_trait(obj)
|
79
|
+
ancs = respond_to?(:ancestors) ? ancestors : self.class.ancestors
|
80
|
+
ancs.unshift(self)
|
81
|
+
ancs.reverse_each{|anc| yield(obj, TRAITS[anc]) if TRAITS.key?(anc) }
|
82
|
+
obj
|
83
|
+
end
|
84
|
+
|
85
|
+
# trait for self.class if we are an instance
|
86
|
+
def class_trait
|
87
|
+
respond_to?(:ancestors) ? trait : self.class.trait
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|