objectified_sessions 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|