active_model_serializer_plus 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.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +3 -0
  3. data/Appraisals +3 -0
  4. data/CHANGELOG.md +11 -0
  5. data/LICENSE.md +22 -0
  6. data/README.md +142 -0
  7. data/Rakefile +7 -0
  8. data/active_model_serializer_plus.gemspec +37 -0
  9. data/gemfiles/rails_4.2.gemfile +7 -0
  10. data/lib/active_model_serializer_plus.rb +4 -0
  11. data/lib/active_model_serializer_plus/assignment.rb +64 -0
  12. data/lib/active_model_serializer_plus/railtie.rb +7 -0
  13. data/lib/active_model_serializer_plus/translations.rb +218 -0
  14. data/lib/active_model_serializer_plus/version.rb +6 -0
  15. data/test/active_model_serializer_plus/active_model_serializer_plus_test.rb +10 -0
  16. data/test/active_model_serializer_plus/assignment_test.rb +64 -0
  17. data/test/active_model_serializer_plus/translations_test.rb +260 -0
  18. data/test/app/rails_4.2/Rakefile +6 -0
  19. data/test/app/rails_4.2/app/assets/javascripts/application.js +16 -0
  20. data/test/app/rails_4.2/app/assets/stylesheets/application.css +15 -0
  21. data/test/app/rails_4.2/app/controllers/application_controller.rb +5 -0
  22. data/test/app/rails_4.2/app/helpers/application_helper.rb +2 -0
  23. data/test/app/rails_4.2/app/views/layouts/application.html.erb +14 -0
  24. data/test/app/rails_4.2/bin/bundle +3 -0
  25. data/test/app/rails_4.2/bin/rails +8 -0
  26. data/test/app/rails_4.2/bin/rake +8 -0
  27. data/test/app/rails_4.2/bin/setup +29 -0
  28. data/test/app/rails_4.2/bin/spring +15 -0
  29. data/test/app/rails_4.2/config.ru +4 -0
  30. data/test/app/rails_4.2/config/application.rb +26 -0
  31. data/test/app/rails_4.2/config/boot.rb +3 -0
  32. data/test/app/rails_4.2/config/database.yml +25 -0
  33. data/test/app/rails_4.2/config/environment.rb +5 -0
  34. data/test/app/rails_4.2/config/environments/development.rb +41 -0
  35. data/test/app/rails_4.2/config/environments/production.rb +79 -0
  36. data/test/app/rails_4.2/config/environments/test.rb +42 -0
  37. data/test/app/rails_4.2/config/initializers/assets.rb +11 -0
  38. data/test/app/rails_4.2/config/initializers/backtrace_silencers.rb +7 -0
  39. data/test/app/rails_4.2/config/initializers/cookies_serializer.rb +3 -0
  40. data/test/app/rails_4.2/config/initializers/filter_parameter_logging.rb +4 -0
  41. data/test/app/rails_4.2/config/initializers/inflections.rb +16 -0
  42. data/test/app/rails_4.2/config/initializers/mime_types.rb +4 -0
  43. data/test/app/rails_4.2/config/initializers/session_store.rb +3 -0
  44. data/test/app/rails_4.2/config/initializers/wrap_parameters.rb +14 -0
  45. data/test/app/rails_4.2/config/locales/en.yml +23 -0
  46. data/test/app/rails_4.2/config/routes.rb +56 -0
  47. data/test/app/rails_4.2/config/secrets.yml +22 -0
  48. data/test/app/rails_4.2/db/schema.rb +0 -0
  49. data/test/app/rails_4.2/db/seeds.rb +7 -0
  50. data/test/app/rails_4.2/public/404.html +67 -0
  51. data/test/app/rails_4.2/public/422.html +67 -0
  52. data/test/app/rails_4.2/public/500.html +66 -0
  53. data/test/app/rails_4.2/public/favicon.ico +0 -0
  54. data/test/app/rails_4.2/public/robots.txt +5 -0
  55. data/test/test_helper.rb +20 -0
  56. metadata +259 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 361857f5e46f0a701cf41f9d43fa7d7f404bf928
4
+ data.tar.gz: a59df64844aa6b4d4b7421263c8c98cb0104f69e
5
+ SHA512:
6
+ metadata.gz: 1f4f2018d56e59590fae7c2facc0160cc7971b55bff6e78069f05a8e26ac2bdabd783e60a4a6ace40d56fb542682d49566bda9ac860c2a4325926b3507aec072
7
+ data.tar.gz: 13e288511f09e66054680471fa61e51bac872308ac4f46595b46b898be86706ba5cd76a90a0400f9d68618d6954384788c28aec796d24ccd6a25e36d51168edd
data/.yardopts ADDED
@@ -0,0 +1,3 @@
1
+ -
2
+ LICENSE.md
3
+ CHANGELOG.md
data/Appraisals ADDED
@@ -0,0 +1,3 @@
1
+ appraise "rails-4.2" do
2
+ gem "rails", "4.2.4"
3
+ end
data/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # 1.0.0
2
+
3
+ * Initial release
4
+
5
+ # 0.9.0
6
+
7
+ * Final development version with complete tests
8
+
9
+ # 0.1.0
10
+
11
+ * Initial development version
data/LICENSE.md ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Todd Knarr
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,142 @@
1
+ # @author Todd Knarr <tknarr@silverglass.org>
2
+
3
+ # ActiveModelSerializerPlus
4
+
5
+ Enhances the standard ActiveModel::Serializers::JSON and ActiveModel::Serializers::Xml
6
+ modules by adding a default `#attributes=` method that implements the normal loop used to
7
+ assign values to attributes. The loop makes use of the `#attribute_types` hash to convert
8
+ sub-hashes to objects of the right class and to parse strings into values of the right
9
+ type (eg. to insure a string containing a time value is converted into a Time object).
10
+ This allows for automatic deserialization of serialized objects without needing to write
11
+ code for it.
12
+
13
+ ## Installation
14
+
15
+ Add this line to your application's Gemfile:
16
+
17
+ ```ruby
18
+ gem 'active_model_serializer_plus'
19
+ ```
20
+
21
+ And then execute:
22
+
23
+ $ bundle
24
+
25
+ Or install it yourself as:
26
+
27
+ $ gem install active_model_serializer_plus
28
+
29
+ ## Usage
30
+
31
+ Add the gem's module to your source file:
32
+
33
+ ```ruby
34
+ require 'active_model_serializer_plus`
35
+ ```
36
+
37
+ Then in the class you need to serialize/deserialize you include the Assignment module
38
+ after including all of the ActiveModel modules you need:
39
+
40
+ ```ruby
41
+ include ActiveModelSerializerPlus::Assignment
42
+ ```
43
+
44
+ The class will need to implement the `#attribute_types` method which should return a hash
45
+ consisting of attribute names and the name of the type/class of the attribute. You only need
46
+ to include those attributes that are themselves a serializable class and that you want turned
47
+ back into objects rather than being left as hashes, or attributes that aren't automatically
48
+ converted from strings back into the correct type (eg. Date, Time, DateTime).
49
+
50
+ This would be an example:
51
+
52
+ ```ruby
53
+ require 'active_model_serializer_plus'
54
+
55
+ class Example
56
+ include ActiveModel::Model
57
+ include ActiveModel::Serializers::JSON
58
+ include ActiveModelSerializerPlus::Assignment
59
+
60
+ attr_accessor :integer_field
61
+ attr_accessor :time_field
62
+ attr_accessor :string_field
63
+ attr_accessor :object_field
64
+
65
+ def attributes
66
+ {
67
+ 'integer_field' => nil,
68
+ 'time_field' => nil,
69
+ 'string_field' => nil,
70
+ 'object_field' => nil
71
+ }
72
+
73
+ def attribute_types
74
+ {
75
+ 'time_field' => 'Time',
76
+ 'object_field' => 'SomeSerializableClass'
77
+ }
78
+
79
+ def initialize( i, t, s, o )
80
+ {
81
+ integer_field = i
82
+ time_field = t
83
+ string_field = s
84
+ object_field = o
85
+ }
86
+
87
+ end
88
+ ```
89
+
90
+ Examples of using this class to serialize an object to a JSON string:
91
+
92
+ obj = Example.new( 5, Time.now, 'xyzzy abc', SomeSerializableClass.new )
93
+ json_string = obj.to_json
94
+
95
+ json_string now contains:
96
+
97
+ { 'integer_field' => 5, 'time_field' => '"2015-09-26T19:04:07-07:00', 'string_field' => 'xyzzy abc',
98
+ 'object_field' => { ... } }
99
+
100
+ And deserializing that string back to an object:
101
+
102
+ new_obj = Example.new.from_json( json_string )
103
+
104
+ new_obj should now be identical to obj, including having time_field being a Time object and
105
+ object_field being a SomeSerializableClass object initialized from the hash in 'object_field'.
106
+
107
+ For ActiveModel classes the types could have been included in the `#attributes` hash, but that would
108
+ conflict with the use of `#attributes` in ActiveRecord classes and could cause confusion.
109
+
110
+ ## Adding information about new types
111
+
112
+ In the `translations.rb` file there are some functions defined on the ActiveModelSerializerPlus module
113
+ itself. The ones you'll probably find useful are `#add_xlate` and `#add_type` which add information about
114
+ a type to the hashes that control formatting and parsing. You'll need to read the documentation on the hashes
115
+ themselves for details, but the short form is that `#add_type` takes the name of a type/class, a Proc that
116
+ takes and object and formats it into a string, a Proc that takes a string and parses it and initializes an
117
+ object from it, and a Proc that takes a deserialized hash and constructs an object from it. `#add_xlate`
118
+ takes the name of a type and the name of it's pseudo-parent class and adds an entry to the type name
119
+ translation table. The lookup routines check for a translation first and see if Procs exist for the
120
+ translated name (the pseudo-parent type). Most of the time you'd omit any new translations and let the
121
+ lookup routines walk up the normal class inheritance chain to find the correct formatting and parsing Procs.
122
+ You'd only fill in translations if you had classes that can be treated as derived from a common class but
123
+ that don't actually derive from any common class. `TrueClass` and `FalseClass` are a good example. They don't
124
+ need a formatting class because they already serialize as `true` and `false` which is convenient, but you'll\
125
+ find a parsing Proc for the pseudo-class `Boolean` that correctly parses both of those strings back to true and
126
+ false values. To set this up you'd do:
127
+
128
+ ```ruby
129
+ add_type('Boolean', nil, Proc.new { |boolean| %w(1 true).include?(boolean.to_s.strip.downcase) }, nil)
130
+ add_xlate('TrueClass', 'Boolean')
131
+ add_xlate('FalseClass', 'Boolean')
132
+ ```
133
+
134
+ which would create the parsing proc entry for `Boolean` and add the translation entries so that `TrueClass`
135
+ and `FalseClass` act as if derived from an imaginary `Boolean` class for formatting and parsing purposes.
136
+
137
+ ## Contributing
138
+
139
+ This project uses `git-flow`, where new features are developed along feature branches based off the
140
+ `develop` branch rather than `master`. Avoid naming your branches `master`, `develop`, `hotfix-`* or `release-`*
141
+ as those conflict with standard branches for hotfixes and releases. You should fork and branch off of the
142
+ `develop` branch instead of `master`, and merge back to `develop` before creating your pull request.
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require 'rubygems'
2
+ require 'bundler/gem_tasks'
3
+ require 'yard'
4
+
5
+ YARD::Rake::YardocTask.new do |t|
6
+ t.options += ['--title', "Active Model Serializer Plus #{ActiveModelSerializerPlus::VERSION} Documentation"]
7
+ end
@@ -0,0 +1,37 @@
1
+ # coding: utf-8
2
+
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'active_model_serializer_plus/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'active_model_serializer_plus'
9
+ spec.version = ActiveModelSerializerPlus::VERSION
10
+ spec.date = Time.now.strftime('%Y-%m-%d')
11
+ spec.author = 'Todd Knarr'
12
+ spec.email = 'tknarr@silverglass.org'
13
+ spec.summary = %q{Enhanced serialization/deserialization support for ActiveModel classes.}
14
+ spec.description = %q{Adds methods for automatic deserialization from standard JSON and XML, and a variant implementation of serialization/deserialization for XML.}
15
+ spec.homepage = 'https://github.com/tknarr/active_model_serializer_plus'
16
+ spec.license = 'MIT'
17
+
18
+ spec.files = Dir.glob('lib/**/*.rb').select { |path| File.file?(path) } +
19
+ [ __FILE__ ]
20
+ spec.test_files = Dir.glob('test/**/*').select { |path| File.file?(path) && !File.fnmatch('*.{log,sqlite3}', path, File::FNM_EXTGLOB) } +
21
+ Dir.glob('gemfiles/*.gemfile') + [ 'Rakefile', 'Appraisals' ]
22
+ spec.extra_rdoc_files = [ 'README.md', 'CHANGELOG.md', 'LICENSE.md', '.yardopts' ]
23
+ spec.require_paths = ['lib']
24
+
25
+ spec.add_dependency 'rails', '~> 4.2'
26
+ spec.add_dependency 'activemodel', '~> 4.2'
27
+
28
+ # Development
29
+ spec.add_development_dependency 'bundler', '~> 1.7'
30
+ spec.add_development_dependency 'rake', '~> 10.0'
31
+ spec.add_development_dependency 'yard', '~> 0'
32
+
33
+ # Testing
34
+ spec.add_development_dependency 'minitest', '~> 5'
35
+ spec.add_development_dependency 'appraisal', '~> 2'
36
+ spec.add_development_dependency 'sqlite3', '~> 1'
37
+ end
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "4.2.4"
6
+
7
+ gemspec :path => "../"
@@ -0,0 +1,4 @@
1
+ require 'active_model_serializer_plus/version'
2
+ require 'active_model_serializer_plus/translations'
3
+ require 'active_model_serializer_plus/assignment'
4
+ require 'active_model_serializer_plus/railtie' if defined?(Rails)
@@ -0,0 +1,64 @@
1
+ require 'active_model_serializer_plus/translations'
2
+
3
+ module ActiveModelSerializerPlus
4
+
5
+ # @author Todd Knarr <tknarr@silverglass.org>
6
+ #
7
+ # Default implementation of the <tt>#attributes=</tt> method for ActiveModel classes.
8
+ #
9
+ # This is the base of the JSON and Xml modules. It supplies a standard <tt>#attributes=</tt> method to
10
+ # classes that follows the basic pattern of the custom one you'd normally write and adds the ability
11
+ # to pick up object type information from the <tt>#attributes_types</tt> hash to convert the hash values of
12
+ # contained objects into actual objects. This mostly eliminates the need to write custom <tt>#attributes=</tt>
13
+ # methods for each class with code to check attribute names and initialize objects from child hashes.
14
+ #
15
+ # The standard ActiveModel/ActiveRecord serialization/deserialization has some flaws, for instance it
16
+ # will serialize classes as contained hashes whether or not they have an <tt>#attributes=</tt> method available
17
+ # to deserialize them. We work around that by having building Procs which can do the correct thing in
18
+ # many cases.
19
+ module Assignment
20
+
21
+ # The default <tt>#attributes=</tt> method which assigns a hash to the object's attributes.
22
+ # @param [Hash] hash the hash of attribute names and values
23
+ # @return [self] self
24
+ # @raise [ArgumentError] if a value from the @attribute_types hash can't be converted to a Class
25
+ def attributes=(hash)
26
+ hash.each do |key, value|
27
+ # Check #attribute_types for what type this item should be, and if we have an entry
28
+ # convert it to an actual Class object we can use.
29
+ klass = nil
30
+ v = nil
31
+ v = self.attribute_types[key] if self.respond_to?(:attribute_types) && self.attribute_types.is_a?(Hash)
32
+ begin
33
+ klass = ActiveModelSerializerPlus.to_class(v) unless v.nil?
34
+ rescue ArgumentError
35
+ raise ArgumentError, "Type #{v.to_s} for attribute #{key.to_s} is not a valid type."
36
+ end
37
+
38
+ unless klass.nil?
39
+ if value.is_a?(Hash)
40
+ # If we're looking at a contained object (our value is a hash), see if we have a Proc
41
+ # to build the object from a hash. If we don't, fall back to creating an object and
42
+ # using the hash #attributes= method if possible. If all else fails just leave the value
43
+ v = ActiveModelSerializerPlus.build(klass.name, value)
44
+ if v.nil? && klass.method_defined?('attributes=')
45
+ v = klass.new
46
+ v.attributes = value
47
+ end
48
+ value = v unless v.nil?
49
+ elsif value.is_a?(String)
50
+ v = ActiveModelSerializerPlus.parse(klass.name, value)
51
+ value = v if v
52
+ end
53
+ end
54
+
55
+ self.send("#{key}=", value)
56
+ end
57
+
58
+ self
59
+
60
+ end
61
+
62
+ end
63
+
64
+ end
@@ -0,0 +1,7 @@
1
+ module ActiveModelSerializerPlus
2
+
3
+ # Railtie class needed for Rails integration.
4
+ class Railtie < Rails::Railtie
5
+ end
6
+
7
+ end
@@ -0,0 +1,218 @@
1
+ # @author Todd Knarr <tknarr@silverglass.org>
2
+ #
3
+ # Top-level namespace for the ActiveModelSerializerPlus extensions.
4
+ #
5
+ # The methods and module variables here aren't commonly used directly by applications, they're
6
+ # mainly for internal use by the Assignment, JSON and Xml namespaces. You'll want to become familiar
7
+ # with the contents if you want to extend the set of types/classes handled automatically during
8
+ # serialization and deserialization.
9
+ #
10
+ # Currently the formatting-related functionality is unused. It's included for use in a planned
11
+ # XML serialization/deserialization extension.
12
+ module ActiveModelSerializerPlus
13
+
14
+ private
15
+
16
+ # Pseudo-parent-class names for classes that can be handled by a common
17
+ # Proc even though they don't derive from a common parent class. That includes
18
+ # container classes, although we don't do anything with them yet.
19
+ @@type_name_xlate = {
20
+ 'TrueClass' => 'Boolean',
21
+ 'FalseClass' => 'Boolean',
22
+ 'Hash' => 'Container',
23
+ 'Array' => 'Container'
24
+ }
25
+
26
+ # If a class is listed here use the Proc to format it into a string, otherwise just use to_s
27
+ @@formatting = {
28
+ 'Date' => Proc.new { |date| date.xmlschema },
29
+ 'DateTime' => Proc.new { |datetime| datetime.xmlschema },
30
+ 'Time' => Proc.new { |time| time.xmlschema }
31
+ }
32
+
33
+ # If a class is listed here, use the Proc to parse a string into an object of the right class
34
+ @@parsing = {
35
+ 'Symbol' => Proc.new { |symbol| symbol.to_sym },
36
+ 'Time' => Proc.new { |time| Time.xmlschema(time) rescue ::Time.parse(time) },
37
+ 'Date' => Proc.new { |date| Date.xmlschema(date) rescue ::Date.parse(date) },
38
+ 'DateTime' => Proc.new { |datetime| DateTime.xmlschema(datetime) rescue ::DateTime.parse(datetime) },
39
+ 'Integer' => Proc.new { |integer| integer.to_i },
40
+ 'Float' => Proc.new { |float| float.to_f },
41
+ 'BigDecimal' => Proc.new { |number| BigDecimal(number) },
42
+ 'Boolean' => Proc.new { |boolean| %w(1 true).include?(boolean.to_s.strip.downcase) }
43
+ }
44
+
45
+ # If a class is listed here, use the Proc to build it from a Hash, otherwise use the
46
+ # standard hash initialization method.
47
+ #
48
+ # This is used to deal with really unpleasant cases where a class is serialized as a hash but
49
+ # can't be constructed from a hash or the values in the hash and we can't for whatever reason
50
+ # insert a new method into the class to do the job.
51
+ @@building = {
52
+ 'IPAddr' => Proc.new { |addr_hash|
53
+ fam = addr_hash['family']
54
+ addr = addr_hash['addr']
55
+ mask = addr_hash['mask_addr']
56
+ # Kludgy. IPAddr has no way to build from the hash or it's contents so we depend
57
+ # on #new accepting an integer address parameter correctly.
58
+ result = IPAddr.new(addr, fam)
59
+ # Kludgy. All we have is the integer netmask but we need either the prefix length as an
60
+ # integer or the netmask as a string of octets. So we use the same #new behavior as above
61
+ # to take the netmask integer value and convert it to a netmask string through an IPAddr
62
+ # object.
63
+ netmask = IPAddr.new(mask,fam).to_s
64
+ result.mask(netmask)
65
+ }
66
+ }
67
+
68
+ public
69
+
70
+ # @!scope class
71
+
72
+ # Translate a type/class name to it's psuedo-parent class name if it has one.
73
+ # @param [String] class_name the name of the class to translate
74
+ # @return [String] the translated name
75
+ def self.type_name_xlate(class_name)
76
+ @@type_name_xlate[class_name]
77
+ end
78
+
79
+ # Format a value into a string using the defined formatting Proc for <tt>value</tt>'s class.
80
+ # @note If no formatting Proc is defined, <tt>#to_s</tt> is used instead.
81
+ # @note Checks all parent classes of <tt>value</tt> for Procs.
82
+ # @param [Object] value the value to format
83
+ # @return [String] the value formatted into a string
84
+ def self.format(value)
85
+ # Check the translation table first for a pseudo-parent, and if we found one check
86
+ # for it's proc. Then start with the value's class name and work up through it's
87
+ # parent classes until we find a proc for one. If we can't find a proc, just use
88
+ # the #to_s method.
89
+ p = nil
90
+ n = @@type_name_xlate[value.class.name]
91
+ p = @@formatting[n] unless n.nil?
92
+ if p.nil?
93
+ b = value.class
94
+ until b.nil? do
95
+ p = @@formatting[b.name]
96
+ break unless p.nil?
97
+ b = b.superclass
98
+ end
99
+ end
100
+ p ? p.call(value) : value.to_s
101
+ end
102
+
103
+ # Parse a string value into an object of the named type if a parsing Proc is defined.
104
+ # @note Checks all parent classes of <tt>class_name</tt> for Procs.
105
+ # @param [String] class_name name of the class of object being created from the value
106
+ # @param [String] value the string containing the formatted value to be parsed
107
+ # @return [Object] the new object, or nil if no parsing Proc for <tt>class_name</tt> is defined
108
+ def self.parse(class_name, value)
109
+ # Check for a proc for the type name given, and if we find one use it. Otherwise
110
+ # try to convert that name to a class. If we can, start with it and work up through
111
+ # it's parent classes checking for a proc for each. If we can't find a proc, return
112
+ # nil.
113
+ p = nil
114
+ xlt = @@type_name_xlate[class_name] || class_name
115
+ p = @@parsing[xlt] unless xlt.nil?
116
+ if p.nil?
117
+ klass = nil
118
+ begin
119
+ klass = class_name.constantize
120
+ rescue NameError
121
+ klass = nil
122
+ end
123
+ until klass.nil?
124
+ p = @@parsing[klass.name]
125
+ break unless p.nil?
126
+ klass = klass.superclass
127
+ end
128
+ end
129
+ p ? p.call(value.to_s) : nil
130
+ end
131
+
132
+ # Build an object of the named type from a hash if a building Proc is defined.
133
+ # @param [String] class_name name of the class of object being built from the hash
134
+ # @param [Hash] hash hash containing the attribute names and values from which the object is to be built
135
+ # @return [Object] the new object, or nil if no building Proc for <tt>class_name</tt> is defined
136
+ def self.build(class_name, hash)
137
+ p = nil
138
+ xlt = @@type_name_xlate[class_name] || class_name
139
+ p = @@building[xlt] unless xlt.nil?
140
+ p ? p.call(hash) : nil
141
+ end
142
+
143
+ # Add a new type translation.
144
+ # Translations are used to create pseudo-parent classes in cases where several classes can use common
145
+ # Procs for formatting, parsing and/or building but don't share a common parent class, eg. TrueClass
146
+ # and FalseClass which can both use the Boolean formatting and parsing Procs.
147
+ # @param [String] type_name name of the specific type
148
+ # @param [String] parent_type_name name of the pseudo-parent type
149
+ # @return [void] no return value
150
+ def self.add_xlate( type_name, parent_type_name )
151
+ return if type_name.blank?
152
+ @@type_name_xlate[type_name] = parent_type_name unless parent_type_name.blank?
153
+ return
154
+ end
155
+
156
+ # Add information about a new type to the formatting/parsing/building hashes.
157
+ # <tt>type_name</tt> is required, all others are optional and should be specified as <tt>nil</tt>
158
+ # if not being defined.
159
+ # @param [String] type_name name of the type to add
160
+ # @param [Proc] formatting_proc Proc to add to format an object's value into a string
161
+ # @param [Proc] parsing_proc Proc to add to parse a string value and create an object from it
162
+ # @param [Proc] building_proc Proc to add to create an object from a hash of attribute names and values
163
+ # @return [void] no return value
164
+ def self.add_type(type_name, formatting_proc = nil, parsing_proc = nil, building_proc = nil)
165
+ return if type_name.blank?
166
+ @@formatting[type_name] = formatting_proc unless formatting_proc.nil?
167
+ @@parsing[type_name] = parsing_proc unless parsing_proc.nil?
168
+ @@building[type_name] = building_proc unless building_proc.nil?
169
+ return
170
+ end
171
+
172
+ # Helper method to convert a type/class name into an actual class.
173
+ # @param [Class, Symbol, String] class_name name to convert into a Class object
174
+ # @return [Class] Class object for the named class
175
+ # @raise [ArgumentError] if <tt>class_name</tt> is not of an acceptable type
176
+ # @raise [NameError] if <tt>class_name</tt> is a Class that doesn't exist
177
+ def self.to_class(class_name)
178
+ klass = nil
179
+ if class_name.is_a?(Class)
180
+ klass = class_name
181
+ elsif class_name.is_a?(String)
182
+ begin
183
+ klass = class_name.constantize unless class_name.blank?
184
+ rescue NameError
185
+ raise ArgumentError, "Type #{class_name} is invalid"
186
+ end
187
+ elsif class_name.is_a?(Symbol)
188
+ begin
189
+ klass = class_name.to_s.constantize
190
+ rescue NameError
191
+ raise ArgumentError, "Type #{class_name.to_s} is invalid"
192
+ end
193
+ else
194
+ raise ArgumentError, "Type #{class_name.to_s} is invalid"
195
+ end
196
+ klass
197
+ end
198
+
199
+ # Helper method to convert a representation of a class or class name into a string containing the class name.
200
+ # @param [Class, Symbol, String] class_name the Class whose name you need or a symbol or string representing a class name
201
+ # @return [String] the class name represented by <tt>class_name</tt> converted to a string
202
+ # @raise [ArgumentError] if <tt>class_name</tt> is not of an acceptable type
203
+ # @raise [NameError] if <tt>class_name</tt> is a Class that doesn't exist
204
+ def self.to_classname(class_name)
205
+ klassname = nil
206
+ if class_name.is_a?(Class)
207
+ klassname = class_name.name
208
+ elsif class_name.is_a?(String)
209
+ klassname = class_name
210
+ elsif class_name.is_a?(Symbol)
211
+ klassname = class_name.to_s
212
+ else
213
+ raise ArgumentError, "Argument type #{class_name.class.name} is not Class, String or Symbol"
214
+ end
215
+ klassname
216
+ end
217
+
218
+ end