objectified_sessions 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (30) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.travis.yml +21 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +391 -0
  7. data/Rakefile +6 -0
  8. data/lib/objectified_session_generator.rb +139 -0
  9. data/lib/objectified_sessions/base.rb +299 -0
  10. data/lib/objectified_sessions/errors.rb +55 -0
  11. data/lib/objectified_sessions/field_definition.rb +105 -0
  12. data/lib/objectified_sessions/version.rb +3 -0
  13. data/lib/objectified_sessions.rb +157 -0
  14. data/objectified_sessions.gemspec +43 -0
  15. data/spec/objectified_sessions/helpers/controller_helper.rb +55 -0
  16. data/spec/objectified_sessions/helpers/exception_helpers.rb +20 -0
  17. data/spec/objectified_sessions/system/basic_system_spec.rb +135 -0
  18. data/spec/objectified_sessions/system/error_handling_system_spec.rb +217 -0
  19. data/spec/objectified_sessions/system/prefix_system_spec.rb +62 -0
  20. data/spec/objectified_sessions/system/retired_inactive_system_spec.rb +188 -0
  21. data/spec/objectified_sessions/system/setup_system_spec.rb +65 -0
  22. data/spec/objectified_sessions/system/strings_symbols_system_spec.rb +73 -0
  23. data/spec/objectified_sessions/system/unknown_data_system_spec.rb +65 -0
  24. data/spec/objectified_sessions/system/visibility_system_spec.rb +61 -0
  25. data/spec/objectified_sessions/unit/objectified_session_generator_spec.rb +121 -0
  26. data/spec/objectified_sessions/unit/objectified_sessions/base_spec.rb +484 -0
  27. data/spec/objectified_sessions/unit/objectified_sessions/errors_spec.rb +75 -0
  28. data/spec/objectified_sessions/unit/objectified_sessions/field_definition_spec.rb +138 -0
  29. data/spec/objectified_sessions/unit/objectified_sessions_spec.rb +149 -0
  30. 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,3 @@
1
+ module ObjectifiedSessions
2
+ VERSION = "1.0.0"
3
+ 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