objectified_sessions 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +21 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +391 -0
- data/Rakefile +6 -0
- data/lib/objectified_session_generator.rb +139 -0
- data/lib/objectified_sessions/base.rb +299 -0
- data/lib/objectified_sessions/errors.rb +55 -0
- data/lib/objectified_sessions/field_definition.rb +105 -0
- data/lib/objectified_sessions/version.rb +3 -0
- data/lib/objectified_sessions.rb +157 -0
- data/objectified_sessions.gemspec +43 -0
- data/spec/objectified_sessions/helpers/controller_helper.rb +55 -0
- data/spec/objectified_sessions/helpers/exception_helpers.rb +20 -0
- data/spec/objectified_sessions/system/basic_system_spec.rb +135 -0
- data/spec/objectified_sessions/system/error_handling_system_spec.rb +217 -0
- data/spec/objectified_sessions/system/prefix_system_spec.rb +62 -0
- data/spec/objectified_sessions/system/retired_inactive_system_spec.rb +188 -0
- data/spec/objectified_sessions/system/setup_system_spec.rb +65 -0
- data/spec/objectified_sessions/system/strings_symbols_system_spec.rb +73 -0
- data/spec/objectified_sessions/system/unknown_data_system_spec.rb +65 -0
- data/spec/objectified_sessions/system/visibility_system_spec.rb +61 -0
- data/spec/objectified_sessions/unit/objectified_session_generator_spec.rb +121 -0
- data/spec/objectified_sessions/unit/objectified_sessions/base_spec.rb +484 -0
- data/spec/objectified_sessions/unit/objectified_sessions/errors_spec.rb +75 -0
- data/spec/objectified_sessions/unit/objectified_sessions/field_definition_spec.rb +138 -0
- data/spec/objectified_sessions/unit/objectified_sessions_spec.rb +149 -0
- metadata +120 -0
@@ -0,0 +1,299 @@
|
|
1
|
+
require 'objectified_sessions'
|
2
|
+
require 'objectified_sessions/field_definition'
|
3
|
+
require 'objectified_sessions/errors'
|
4
|
+
|
5
|
+
module ObjectifiedSessions
|
6
|
+
# ObjectifiedSessions::Base is the base class for all objectified sessions -- in other words, all classes that
|
7
|
+
# actually implement an objectified session must inherit from this class. It therefore contains the methods that
|
8
|
+
# allow you to define new fields, set various options (like #unknown_fields and #default_visibility), and so on.
|
9
|
+
#
|
10
|
+
# Most functionality here is actually implemented on the class itself (the <tt>class << self</tt> block below), as
|
11
|
+
# most of the functionality has to do with defining which fields exist, how they should behave, and so on.
|
12
|
+
# Behavior for an actual instance is smaller, and largely limited to reading and writing data from fields, as
|
13
|
+
# most such access comes through dynamically-generated methods via the class.
|
14
|
+
class Base
|
15
|
+
# Creates a new instance. +underlying_session+ is the Rails session object -- _i.e._, whatever is returned by
|
16
|
+
# calling #session in a controller. (The actual class of this object varies among Rails versions, but its
|
17
|
+
# behavior is identical for our purposes.)
|
18
|
+
#
|
19
|
+
# This method also takes care of calling #_delete_unknown_fields_if_needed!, which, as its name suggests, is
|
20
|
+
# responsible for deleting any data that does not map to any known fields.
|
21
|
+
def initialize(underlying_session)
|
22
|
+
@_base_underlying_session = underlying_session
|
23
|
+
_delete_unknown_fields_if_needed!
|
24
|
+
end
|
25
|
+
|
26
|
+
# A convenient alias for accessible_field_names, so you don't have to go through the class.
|
27
|
+
def field_names
|
28
|
+
self.class.accessible_field_names
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns the (possibly empty) set of all field names that actually have data present.
|
32
|
+
def keys
|
33
|
+
field_names.select { |f| self[f] != nil }
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns a nice, pretty string of the current set of values for this session. We abbreviate long values by default,
|
37
|
+
# so that we don't return some absurdly-long string.
|
38
|
+
def to_s(abbreviate = true)
|
39
|
+
out = "<#{self.class.name}: "
|
40
|
+
|
41
|
+
out << keys.sort_by(&:to_s).map do |k|
|
42
|
+
s = self[k].inspect
|
43
|
+
s = s[0..36] + "..." if abbreviate && s.length > 40
|
44
|
+
"#{k}: #{s}"
|
45
|
+
end.join(", ")
|
46
|
+
|
47
|
+
out << ">"
|
48
|
+
out
|
49
|
+
end
|
50
|
+
|
51
|
+
# Make #inspect do the same as #to_s, so we also get this in debugging output.
|
52
|
+
def inspect(abbreviate = true)
|
53
|
+
to_s(abbreviate)
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
# This method returns the 'true' underlying session we should use. Typically this is nothing more than
|
58
|
+
# +@_base_underlying_session+ -- the argument passed in to our constructor -- but, if a prefix is set, this is
|
59
|
+
# responsible for fetching the "sub-session" Hash we should use to store all our data, instead.
|
60
|
+
#
|
61
|
+
# +create_if_needed+ should be set to +true+ if, when calling this method, we should create the prefixed
|
62
|
+
# "sub-session" Hash if it's not present. If +false+, this method can return +nil+, if there is no
|
63
|
+
# prefixed sub-session. We allow this parameter to make sure we don't bind an empty Hash to our prefix if there's
|
64
|
+
# nothing to store in it anyway.
|
65
|
+
def _objectified_sessions_underlying_session(create_if_needed)
|
66
|
+
prefix = self.class.prefix
|
67
|
+
|
68
|
+
if prefix
|
69
|
+
out = @_base_underlying_session[prefix]
|
70
|
+
|
71
|
+
if (! out) && create_if_needed
|
72
|
+
@_base_underlying_session[prefix] = { }
|
73
|
+
out = @_base_underlying_session[prefix]
|
74
|
+
end
|
75
|
+
|
76
|
+
out
|
77
|
+
else
|
78
|
+
@_base_underlying_session
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Takes care of deleting any unknown fields, if unknown_fields == :delete.
|
83
|
+
def _delete_unknown_fields_if_needed!
|
84
|
+
if self.class.unknown_fields == :delete
|
85
|
+
underlying = _objectified_sessions_underlying_session(false)
|
86
|
+
|
87
|
+
if underlying # can be nil, if there's a prefix and nothing stored in it yet
|
88
|
+
# Find all keys that either don't map to a field, or map to a field with #delete_data_with_storage_name? =>
|
89
|
+
# true -- that is, retired fields.
|
90
|
+
unknown = underlying.keys.select do |k|
|
91
|
+
field = self.class._field_with_storage_name(k)
|
92
|
+
(! field) || field.delete_data_with_storage_name?
|
93
|
+
end
|
94
|
+
|
95
|
+
underlying.delete(unknown) if unknown.length > 0
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Returns the current value for the field with the given name. +field_name+ can be specified as a String or a
|
101
|
+
# Symbol. Returns nil if nothing has been set yet.
|
102
|
+
#
|
103
|
+
# If passed a field name that hasn't been defined on this class, raises
|
104
|
+
# ObjectifiedSessions::Errors::NoSuchFieldError.
|
105
|
+
def [](field_name)
|
106
|
+
field = self.class._ensure_has_field_named(field_name)
|
107
|
+
|
108
|
+
underlying = _objectified_sessions_underlying_session(false)
|
109
|
+
underlying[field.storage_name] if underlying
|
110
|
+
end
|
111
|
+
|
112
|
+
# Stores a new value to the given field. +field_name+ can be specified as a String or a Symbol. If passed +nil+,
|
113
|
+
# will store +nil+ to the underlying session, which deletes the given key from the session entirely.
|
114
|
+
#
|
115
|
+
# If passed a field name that hasn't been defined on this class, raises
|
116
|
+
# ObjectifiedSessions::Errors::NoSuchFieldError.
|
117
|
+
def []=(field_name, new_value)
|
118
|
+
field = self.class._ensure_has_field_named(field_name)
|
119
|
+
_objectified_sessions_underlying_session(true)[field.storage_name] = new_value
|
120
|
+
new_value
|
121
|
+
end
|
122
|
+
|
123
|
+
DYNAMIC_METHODS_MODULE_NAME = :ObjectifiedSessionsDynamicMethods
|
124
|
+
|
125
|
+
class << self
|
126
|
+
# Defines a new field. +name+ is the name of the field, specified as either a String or a Symbol. +options+ can
|
127
|
+
# contain:
|
128
|
+
#
|
129
|
+
# [:visibility] If +:private+, methods generated for this field will be marked as private, meaning they can only
|
130
|
+
# be accessed from inside the objectified-session class itself. If +:public+, methods will be
|
131
|
+
# marked as public, making them accessible from anywhere. If omitted, the class's
|
132
|
+
# #default_visibility will be used (which itself defaults to +:public+).
|
133
|
+
# [:storage] If specified, this field will be stored in the session under the given String or Symbol (which will
|
134
|
+
# be converted to a String before being used). If not specified, data will be stored under the name of
|
135
|
+
# the field (converted to a String), instead.
|
136
|
+
def field(name, options = { })
|
137
|
+
@fields ||= { }
|
138
|
+
@fields_by_storage_name ||= { }
|
139
|
+
|
140
|
+
# Compute our effective options.
|
141
|
+
options = { :visibility => default_visibility }.merge(options)
|
142
|
+
options[:type] ||= :normal
|
143
|
+
|
144
|
+
# Create a new FieldDefinition instance.
|
145
|
+
new_field = ObjectifiedSessions::FieldDefinition.new(self, name, options)
|
146
|
+
|
147
|
+
# Check for a conflict with the field name.
|
148
|
+
if @fields[new_field.name]
|
149
|
+
raise ObjectifiedSessions::Errors::DuplicateFieldNameError.new(self, new_field.name)
|
150
|
+
end
|
151
|
+
|
152
|
+
# Check for a conflict with the storage name.
|
153
|
+
if @fields_by_storage_name[new_field.storage_name]
|
154
|
+
raise ObjectifiedSessions::Errors::DuplicateFieldStorageNameError.new(self, @fields_by_storage_name[new_field.storage_name].name, new_field.name, new_field.storage_name)
|
155
|
+
end
|
156
|
+
|
157
|
+
@fields[new_field.name] = new_field
|
158
|
+
@fields_by_storage_name[new_field.storage_name] = new_field
|
159
|
+
end
|
160
|
+
|
161
|
+
# Defines a retired field. A retired field is really nothing more than a marker indicating that you _used_ to
|
162
|
+
# have a field with a given name (and, potentially, storage alias); you can't access its data, and, if
|
163
|
+
# you've set <tt>unknown_fields :delete</tt>, any data _will_ be deleted.
|
164
|
+
#
|
165
|
+
# So, what's the point? You will still get an error if you try to define another field with the same name, or
|
166
|
+
# storage alias. If you re-use a field, then, especially if you're using Rails' default CookieStore, you
|
167
|
+
# can run into awful problems where data from some previous usage is interpreted as being valid data for the new
|
168
|
+
# usage. Instead of simply removing fields when you're done with them, make them retired (and move them to the
|
169
|
+
# bottom of the class, if you want, for better readability); this will have the same effect as removing them,
|
170
|
+
# but will keep you from accidentally reusing them in the future.
|
171
|
+
#
|
172
|
+
# +name+ is the name of the field; the only valid option for +options+ is +:storage+. (+:visibility+ is accepted
|
173
|
+
# but ignored, since no methods are generated for retired fields.)
|
174
|
+
def retired(name, options = { })
|
175
|
+
field(name, options.merge(:type => :retired))
|
176
|
+
end
|
177
|
+
|
178
|
+
# Defines an inactive field. An inactive field is identical to a retired field, except that, if you've set
|
179
|
+
# <tt>unknown_fields :delete</tt>, data from an inactive field will _not_ be deleted. You can use it as a way of
|
180
|
+
# retiring a field that you no longer want to use from code, but whose data you still want preserved. (If you
|
181
|
+
# have not set <tt>unknown_fields :delete</tt>, then it behaves identically to a retired field.)
|
182
|
+
#
|
183
|
+
# +name+ is the name of the field; the only valid option for +options+ is +:storage+. (+:visibility+ is accepted
|
184
|
+
# but ignored, since no methods are generated for inactive fields.)
|
185
|
+
def inactive(name, options = { })
|
186
|
+
field(name, options.merge(:type => :inactive))
|
187
|
+
end
|
188
|
+
|
189
|
+
# Sets the default visibility of new fields on this class. This is ordinarily +:public+, meaning fields will
|
190
|
+
# generate accessor methods (_e.g._, +#foo+ and +#foo=+) that are public unless you explicitly say
|
191
|
+
# <tt>:visibility => :private</tt> in the field definition. However, you can change it to +:private+, meaning
|
192
|
+
# fields will be private unless you explicitly specify <tt>:visibility => :public</tt>.
|
193
|
+
#
|
194
|
+
# If called without an argument, returns the current default visibility for fields on this class.
|
195
|
+
def default_visibility(new_visibility = nil)
|
196
|
+
if new_visibility
|
197
|
+
if [ :public, :private ].include?(new_visibility)
|
198
|
+
@default_visibility = new_visibility
|
199
|
+
else
|
200
|
+
raise ArgumentError, "Invalid default visibility: #{new_visibility.inspect}; must be :public or :private"
|
201
|
+
end
|
202
|
+
else
|
203
|
+
@default_visibility ||= :public
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
# Sets the prefix. If a prefix is set, then all field data is taken from (and stored into) a Hash bound to this
|
208
|
+
# prefix within the session, rather than directly in the session; this segregates all your ObjectifiedSession
|
209
|
+
# data from other usage of the session. This is not generally necessary, but can be useful in certain situations.
|
210
|
+
# Note that setting a prefix affects _all_ fields, not just those defined after it's set; the prefix is global
|
211
|
+
# to your objectified session, and you can only have a single prefix at once.
|
212
|
+
#
|
213
|
+
# Perhaps obvious, but changing the prefix will effectively cause all your objectified-session data to disappear,
|
214
|
+
# as it'll be stored under a different key. Choose once, at the beginning.
|
215
|
+
#
|
216
|
+
# If called with no arguments, returns the current prefix.
|
217
|
+
def prefix(new_prefix = :__none_specified)
|
218
|
+
if new_prefix == :__none_specified
|
219
|
+
@prefix
|
220
|
+
elsif new_prefix.kind_of?(String) || new_prefix.kind_of?(Symbol) || new_prefix == nil
|
221
|
+
@prefix = if new_prefix then new_prefix.to_s.strip else nil end
|
222
|
+
else
|
223
|
+
raise ArgumentError, "Invalid prefix; must be a String or Symbol: #{new_prefix.inspect}"
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
# Sets what to do with unknown fields. With +:preserve+, the default setting, any data residing under keys that
|
228
|
+
# aren't defined as a field will simply be preserved, even as it's inaccessible. With +:delete+, any data
|
229
|
+
# residing under keys that aren't defined as a field will be *deleted* when your objectified session class is
|
230
|
+
# instantiated. Obviously, be careful if you set this to +:delete+; if you're using traditional session access
|
231
|
+
# anywhere else in code, and you don't duplicate its use as a field in your objectified session, really bad things
|
232
|
+
# will happen as the objectified session removes keys being used by other parts of the code. But it's a very nice
|
233
|
+
# way to keep your session tidy, too.
|
234
|
+
def unknown_fields(what_to_do = nil)
|
235
|
+
if what_to_do == nil
|
236
|
+
@unknown_fields ||= :preserve
|
237
|
+
elsif [ :delete, :preserve ].include?(what_to_do)
|
238
|
+
@unknown_fields = what_to_do
|
239
|
+
else
|
240
|
+
raise ArgumentError, "You must pass :delete or :preserve, not: #{what_to_do.inspect}"
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
# What are the names of all fields that are accessible -- that is, whose data can be accessed? This returns an
|
245
|
+
# array of field names, not storage names; retired fields and inactive fields don't allow access to their data,
|
246
|
+
# so they won't be included.
|
247
|
+
def accessible_field_names
|
248
|
+
if @fields
|
249
|
+
@fields.values.select { |f| f.allow_access_to_data? }.map(&:name)
|
250
|
+
else
|
251
|
+
[ ]
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
# Returns the FieldDefinition object with the given name, if any.
|
256
|
+
def _field_named(name)
|
257
|
+
name = ObjectifiedSessions::FieldDefinition.normalize_name(name)
|
258
|
+
@fields[name]
|
259
|
+
end
|
260
|
+
|
261
|
+
# Returns the FieldDefinition object that stores its data under the given key, if any.
|
262
|
+
def _field_with_storage_name(storage_name)
|
263
|
+
storage_name = ObjectifiedSessions::FieldDefinition.normalize_name(storage_name).to_s
|
264
|
+
@fields_by_storage_name[storage_name]
|
265
|
+
end
|
266
|
+
|
267
|
+
# If this class doesn't have an active field (not retired or inactive) with the given name, raises
|
268
|
+
# ObjectifiedSessions::Errors::NoSuchFieldError. This is used as a guard to make sure we don't try to retrieve
|
269
|
+
# data that hasn't been defined as a field.
|
270
|
+
def _ensure_has_field_named(name)
|
271
|
+
out = _field_named(name)
|
272
|
+
out = nil if out && (! out.allow_access_to_data?)
|
273
|
+
out || (raise ObjectifiedSessions::Errors::NoSuchFieldError.new(self, name))
|
274
|
+
end
|
275
|
+
|
276
|
+
# Returns the dynamic-methods module. The dynamic-methods module is a new Module that is automatically included
|
277
|
+
# into the objectified-sessions class and given a reasonable name; it also has #define_method and #private made
|
278
|
+
# into public methods, so that it's easy to define methods on it.
|
279
|
+
#
|
280
|
+
# The dynamic-methods module is where we define all the accessor methods that #field generates. We do this instead
|
281
|
+
# of defining them directly on this class so that you can override them, and #super will still work properly.
|
282
|
+
def _dynamic_methods_module
|
283
|
+
@_dynamic_methods_module ||= begin
|
284
|
+
out = Module.new do
|
285
|
+
class << self
|
286
|
+
public :define_method, :private
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
remove_const(DYNAMIC_METHODS_MODULE_NAME) if const_defined?(DYNAMIC_METHODS_MODULE_NAME)
|
291
|
+
const_set(DYNAMIC_METHODS_MODULE_NAME, out)
|
292
|
+
|
293
|
+
include out
|
294
|
+
out
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module ObjectifiedSessions
|
2
|
+
# This module contains definitions of errors for ObjectifiedSessions.
|
3
|
+
module Errors
|
4
|
+
# The base class from which all ObjectifiedSessions errors descend.
|
5
|
+
class Base < StandardError; end
|
6
|
+
|
7
|
+
# Raised when we cannot create the ObjectifiedSessions subclass, when someone calls #objsession; this usually means
|
8
|
+
# either the subclass hasn't been defined, or its constructor, for some reason, raised an error.
|
9
|
+
class CannotCreateSessionError < Base; end
|
10
|
+
|
11
|
+
# Raised when you try to read or write a field (via hash-indexing) that simply isn't defined on your objectified
|
12
|
+
# session.
|
13
|
+
class NoSuchFieldError < Base
|
14
|
+
attr_reader :session_class, :field_name
|
15
|
+
|
16
|
+
def initialize(session_class, field_name)
|
17
|
+
@session_class = session_class
|
18
|
+
@field_name = field_name
|
19
|
+
|
20
|
+
super("Class #{@session_class.name} has no field named #{@field_name.inspect}; its fields are: #{accessible_field_names.inspect}")
|
21
|
+
end
|
22
|
+
|
23
|
+
def accessible_field_names
|
24
|
+
session_class.accessible_field_names
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Raised when you try to define a field that has the same name as a previously-defined field.
|
29
|
+
class DuplicateFieldNameError < Base
|
30
|
+
attr_reader :session_class, :field_name
|
31
|
+
|
32
|
+
def initialize(session_class, field_name)
|
33
|
+
@session_class = session_class
|
34
|
+
@field_name = field_name
|
35
|
+
|
36
|
+
super("Class #{@session_class.name} already has one field named #{@field_name.inspect}; you can't define another.")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Raised when you try to define a field that has a different name, but the same storage name, as a previously-defined
|
41
|
+
# field.
|
42
|
+
class DuplicateFieldStorageNameError < Base
|
43
|
+
attr_reader :session_class, :original_field_name, :new_field_name, :storage_name
|
44
|
+
|
45
|
+
def initialize(session_class, original_field_name, new_field_name, storage_name)
|
46
|
+
@session_class = session_class
|
47
|
+
@original_field_name = original_field_name
|
48
|
+
@new_field_name = new_field_name
|
49
|
+
@storage_name = storage_name
|
50
|
+
|
51
|
+
super("Class #{@session_class.name} already has a field, #{@original_field_name.inspect}, with storage name #{@storage_name.inspect}; you can't define field #{@new_field_name.inspect} with that same storage name.")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module ObjectifiedSessions
|
2
|
+
# A FieldDefinition represents, well, the definition of a single field against a single class (which must be a
|
3
|
+
# descendant of ObjectifiedSessions::Base). It knows how to respond to a few questions, and is responsible for
|
4
|
+
# creating appropriate delegated methods in its owning class's +_dynamic_methods_module+.
|
5
|
+
class FieldDefinition
|
6
|
+
class << self
|
7
|
+
# Normalizes the name of a field. We use this method to make sure we don't get confused between Strings and
|
8
|
+
# Symbols, and so on.
|
9
|
+
def normalize_name(name)
|
10
|
+
unless name.kind_of?(String) || name.kind_of?(Symbol)
|
11
|
+
raise ArgumentError, "A field name must be a String or Symbol, not: #{name.inspect}"
|
12
|
+
end
|
13
|
+
|
14
|
+
name.to_s.strip.to_sym
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_reader :name, :storage_name
|
19
|
+
|
20
|
+
# Creates a new instance. +session_class+ must be the Class that you're using as your objectified-session class --
|
21
|
+
# _i.e._, a subclass of ObjectifiedSessions::Base. +name+ is the name of the field. +options+ are the options for
|
22
|
+
# the field:
|
23
|
+
#
|
24
|
+
# [:type] Required; must be one of: +:normal+, +:retired+, or +:inactive+, each corresponding to the field type
|
25
|
+
# documented in ObjectifiedSessions::Base.
|
26
|
+
# [:storage] If present, this field will use the specified string as the key under which it should be stored; if
|
27
|
+
# not present, the name will be used instead.
|
28
|
+
# [:visibility] Required; must be +:private+ or +:public+. Methods created on the #_dynamic_methods_module on
|
29
|
+
# the base class will be of this visibility.
|
30
|
+
def initialize(session_class, name, options = { })
|
31
|
+
raise ArgumentError, "Session class must be a Class, not: #{session_class.inspect}" unless session_class.kind_of?(Class)
|
32
|
+
|
33
|
+
@session_class = session_class
|
34
|
+
@name = self.class.normalize_name(name)
|
35
|
+
process_options!(options)
|
36
|
+
|
37
|
+
create_methods!
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns the key under which this field should read and write its data. This will be its name, unless a
|
41
|
+
# +:storage+ option was passed to the constructor, in which case it will be that value, instead.
|
42
|
+
def storage_name
|
43
|
+
@storage_name || name
|
44
|
+
end
|
45
|
+
|
46
|
+
# If someone has set <tt>unknown_fields :delete</tt> on the base class, should we delete data with this field's
|
47
|
+
# #storage_name anyway? This is true only for retired fields.
|
48
|
+
def delete_data_with_storage_name?
|
49
|
+
type == :retired
|
50
|
+
end
|
51
|
+
|
52
|
+
# Should we allow users to access the data in this field? Retired and inactive fields don't allow access to their
|
53
|
+
# data.
|
54
|
+
def allow_access_to_data?
|
55
|
+
type == :normal
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
attr_reader :type, :visibility
|
60
|
+
|
61
|
+
# Process the options passed in; this validates them, and sets +@type+, +@visibility+, and +@storage_name+
|
62
|
+
# appropriately.
|
63
|
+
def process_options!(options)
|
64
|
+
options.assert_valid_keys(:storage, :type, :visibility)
|
65
|
+
|
66
|
+
case options[:storage]
|
67
|
+
when nil, String, Symbol then nil
|
68
|
+
else raise ArgumentError, "Invalid value for :storage: #{options[:storage].inspect}"
|
69
|
+
end
|
70
|
+
|
71
|
+
if options[:storage]
|
72
|
+
@storage_name = self.class.normalize_name(options[:storage]).to_s
|
73
|
+
else
|
74
|
+
@storage_name = self.name.to_s
|
75
|
+
end
|
76
|
+
|
77
|
+
raise ArgumentError, "Invalid value for :type: #{options[:type].inspect}" unless [ :normal, :inactive, :retired ].include?(options[:type])
|
78
|
+
@type = options[:type]
|
79
|
+
|
80
|
+
raise ArgumentError, "Invalid value for :visibility: #{options[:visibility].inspect}" unless [ :public, :private ].include?(options[:visibility])
|
81
|
+
@visibility = options[:visibility]
|
82
|
+
end
|
83
|
+
|
84
|
+
# Creates methods on the dynamic-methods module, as appropriate.
|
85
|
+
def create_methods!
|
86
|
+
return unless type == :normal
|
87
|
+
|
88
|
+
fn = name
|
89
|
+
dmm = @session_class._dynamic_methods_module
|
90
|
+
mn = name.to_s.downcase
|
91
|
+
|
92
|
+
dmm.define_method(mn) do
|
93
|
+
self[fn]
|
94
|
+
end
|
95
|
+
|
96
|
+
dmm.define_method("#{mn}=") do |new_value|
|
97
|
+
self[fn] = new_value
|
98
|
+
end
|
99
|
+
|
100
|
+
if visibility == :private
|
101
|
+
dmm.send(:private, mn, "#{mn}=".to_sym)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
require "action_controller"
|
2
|
+
require "objectified_sessions/version"
|
3
|
+
require "objectified_sessions/base"
|
4
|
+
require "objectified_session_generator"
|
5
|
+
|
6
|
+
# ObjectifiedSessions is the outermost interface to the ObjectifiedSessions Gem. This module exists only as a namespace
|
7
|
+
# (_i.e._, is not included into any classes), and has a single public method, #session_class, that lets you configure
|
8
|
+
# which class is to be used as your +objsession+.
|
9
|
+
module ObjectifiedSessions
|
10
|
+
DEFAULT_OBJSESSION_CLASS_NAME = "Objsession"
|
11
|
+
|
12
|
+
class << self
|
13
|
+
# Should be called from code internal to the ObjectifiedSessions Gem only. Given the underlying Session object
|
14
|
+
# (as returned by `#session` in a controller), creates a new instance of the correct objectified-session class
|
15
|
+
# and returns it.
|
16
|
+
#
|
17
|
+
# This method is actually trivially simple; it's more than two lines just because we want to be careful to raise
|
18
|
+
# good, usable exceptions if there's a problem.
|
19
|
+
def _create_new_objsession(underlying_session)
|
20
|
+
klass = _session_class_object
|
21
|
+
out = nil
|
22
|
+
|
23
|
+
# Create a new instance...
|
24
|
+
begin
|
25
|
+
out = klass.new(underlying_session)
|
26
|
+
rescue Exception => e
|
27
|
+
raise ObjectifiedSessions::Errors::CannotCreateSessionError, %{When objectified_sessions went to create a new instance of the session class, it
|
28
|
+
got an exception from the call to #{klass.name}.new:
|
29
|
+
|
30
|
+
(#{e.class.name}) #{e.message}
|
31
|
+
#{e.backtrace.join("\n ")}}
|
32
|
+
end
|
33
|
+
|
34
|
+
# ...and make sure it's a subclass of ::ObjectifiedSessions::Base.
|
35
|
+
unless out.kind_of?(::ObjectifiedSessions::Base)
|
36
|
+
raise ObjectifiedSessions::Errors::CannotCreateSessionError, %{When objectified_sessions went to create a new instance of the session class, it
|
37
|
+
got back an object that isn't an instance of a subclass of ObjectifiedSessions::Base.
|
38
|
+
|
39
|
+
It got back an instance of #{out.class.name}:
|
40
|
+
#{out.inspect}}
|
41
|
+
end
|
42
|
+
|
43
|
+
out
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns the session class that's been set -- in whatever format it's been set. This means that the return value
|
47
|
+
# can be a String, Symbol, or Class, depending on how the client set it.
|
48
|
+
def session_class
|
49
|
+
@session_class ||= DEFAULT_OBJSESSION_CLASS_NAME
|
50
|
+
end
|
51
|
+
|
52
|
+
# Sets the class that should be instantiated and bound to #objsession in controllers. You can pass a String or
|
53
|
+
# Symbol that's the name of the class, or the actual Class object itself.
|
54
|
+
#
|
55
|
+
# Class loading: if the class is not already loaded, then ObjectifiedSessions will attempt to load it, using
|
56
|
+
# Kernel#require, using a file path that's the Rails-style mapping from the name of the class. (In other words,
|
57
|
+
# if you pass 'Foo::BarBaz' for +target_class+, then ObjectifiedSessions will <tt>require 'foo/bar_baz'</tt>.)
|
58
|
+
#
|
59
|
+
# However specified, the class must be a subclass of ObjectifiedSessions::Base, or you'll get an error when you
|
60
|
+
# call #objsession.
|
61
|
+
#
|
62
|
+
# Note that this is evaluated the first time you call #objsession from within a controller, not immediately. This
|
63
|
+
# means your application will be fully booted and all of Rails available when you do this, but it also means that
|
64
|
+
# if you set a class that can't be resolved or has an error in it, you won't find out until you first try to access
|
65
|
+
# the #objsession. Be aware.
|
66
|
+
def session_class=(target_class)
|
67
|
+
unless [ String, Symbol, Class ].include?(target_class.class)
|
68
|
+
raise ArgumentError, "You must pass a String, Symbol, or Class, not: #{target_class.inspect}"
|
69
|
+
end
|
70
|
+
|
71
|
+
if target_class.kind_of?(String) || target_class.kind_of?(Symbol)
|
72
|
+
target_class = target_class.to_s.camelize
|
73
|
+
end
|
74
|
+
|
75
|
+
@session_class = target_class
|
76
|
+
@_session_class_object = nil
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
# Returns the actual Class object specified by #session_class, above. This is the method that does the work of
|
81
|
+
# resolving a String or Symbol that was passed there.
|
82
|
+
def _session_class_object
|
83
|
+
# We cache this so that we don't call #constantize, a relatively expensive operation, every time we need to
|
84
|
+
# instantiate a new #objsession.
|
85
|
+
@_session_class_object ||= begin
|
86
|
+
klass = session_class
|
87
|
+
|
88
|
+
unless klass.kind_of?(Class)
|
89
|
+
path = nil
|
90
|
+
load_error = nil
|
91
|
+
|
92
|
+
begin
|
93
|
+
# Compute the path this class would have...
|
94
|
+
path = klass.underscore
|
95
|
+
|
96
|
+
# ...and try to Kernel#require it. If we get an error, that's fine; we'll keep going and try to use the
|
97
|
+
# class anyway -- but we'll report on it if that fails, since it's very useful information in debugging.
|
98
|
+
begin
|
99
|
+
require path
|
100
|
+
rescue LoadError => le
|
101
|
+
load_error = le
|
102
|
+
end
|
103
|
+
|
104
|
+
klass = klass.constantize
|
105
|
+
rescue NameError => ne
|
106
|
+
message = nil
|
107
|
+
|
108
|
+
# If you haven't changed the default session-class name, then you probably just haven't run the generator;
|
109
|
+
# let's tell you to do that.
|
110
|
+
if klass.to_s == DEFAULT_OBJSESSION_CLASS_NAME.to_s
|
111
|
+
message = %{Before using objectified_sessions, you need to define the class that implements your
|
112
|
+
objectfied session. By default, this is named #{klass.inspect}; simply create a class of
|
113
|
+
that name, in the appropriate place in your project (e.g., lib/objsession.rb). You can
|
114
|
+
run 'rails generate objectified_session' to do this for you.
|
115
|
+
|
116
|
+
Alternatively, tell objectified_sessions to use a particular class, by saying
|
117
|
+
|
118
|
+
ObjectifiedSessions.session_class = <class name>
|
119
|
+
|
120
|
+
somewhere in your config/application.rb, or some similar initialization code.}
|
121
|
+
else
|
122
|
+
# If you *have* changed the default session-class name, you probably know what you're doing, so let's
|
123
|
+
# give you a different error message.
|
124
|
+
message = %{When objectified_sessions went to create a new instance of the session class, it
|
125
|
+
couldn't resolve the actual class. You specified #{klass.inspect} as the session class,
|
126
|
+
but, when we called #constantize on it, we got the following NameError:
|
127
|
+
|
128
|
+
(#{ne.class.name}) #{ne}}
|
129
|
+
end
|
130
|
+
|
131
|
+
# This is where we add information about the LoadError, above.
|
132
|
+
if load_error
|
133
|
+
message += %{
|
134
|
+
|
135
|
+
(When we tried to require the file presumably containing this class (with 'require #{path.inspect}'),
|
136
|
+
we got a LoadError: #{load_error.message}. This may not be an issue, if you have this class defined elsewhere; in
|
137
|
+
that case, you can simply ignore the error. But it may also indicate that the file you've defined this class in,
|
138
|
+
if any, isn't on the load path.)}
|
139
|
+
end
|
140
|
+
|
141
|
+
raise NameError, message
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
klass
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# This is what actually makes #objsession in a controller work. Returns an instance of whatever class you have specified
|
152
|
+
# as your objectified-session class (usually just named +Objsession+).
|
153
|
+
class ActionController::Base
|
154
|
+
def objsession
|
155
|
+
@_objsession ||= ::ObjectifiedSessions::_create_new_objsession(session)
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'objectified_sessions/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = "objectified_sessions"
|
8
|
+
s.version = ObjectifiedSessions::VERSION
|
9
|
+
s.authors = ["Andrew Geweke"]
|
10
|
+
s.email = ["andrew@geweke.org"]
|
11
|
+
s.description = %q{Encapsulate and carefully manage access to your Rails session.}
|
12
|
+
s.summary = %q{Encapsulate and carefully manage access to your Rails session.}
|
13
|
+
s.homepage = "https://github.com/ageweke/objectified_sessions"
|
14
|
+
s.license = "MIT"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split($/)
|
17
|
+
s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
s.test_files = s.files.grep(%r{^(test|s|features)/})
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
s.add_development_dependency "rake"
|
23
|
+
s.add_development_dependency "rspec", "~> 2.14"
|
24
|
+
|
25
|
+
if (RUBY_VERSION =~ /^1\.9\./ || RUBY_VERSION =~ /^2\.0\./) && ((! defined?(RUBY_ENGINE)) || (RUBY_ENGINE != 'jruby'))
|
26
|
+
s.add_development_dependency "pry"
|
27
|
+
s.add_development_dependency "pry-debugger"
|
28
|
+
s.add_development_dependency "pry-stack_explorer"
|
29
|
+
end
|
30
|
+
|
31
|
+
rails_version = ENV['OBJECTIFIED_SESSIONS_RAILS_TEST_VERSION']
|
32
|
+
rails_version = rails_version.strip if rails_version
|
33
|
+
|
34
|
+
version_spec = case rails_version
|
35
|
+
when nil then [ ">= 3.0", "<= 4.99.99" ]
|
36
|
+
when 'master' then nil
|
37
|
+
else [ "=#{rails_version}" ]
|
38
|
+
end
|
39
|
+
|
40
|
+
if version_spec
|
41
|
+
s.add_dependency("rails", *version_spec)
|
42
|
+
end
|
43
|
+
end
|