active_model_serializer_plus 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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