active_model_serializer_plus 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.yardopts +3 -0
- data/Appraisals +3 -0
- data/CHANGELOG.md +11 -0
- data/LICENSE.md +22 -0
- data/README.md +142 -0
- data/Rakefile +7 -0
- data/active_model_serializer_plus.gemspec +37 -0
- data/gemfiles/rails_4.2.gemfile +7 -0
- data/lib/active_model_serializer_plus.rb +4 -0
- data/lib/active_model_serializer_plus/assignment.rb +64 -0
- data/lib/active_model_serializer_plus/railtie.rb +7 -0
- data/lib/active_model_serializer_plus/translations.rb +218 -0
- data/lib/active_model_serializer_plus/version.rb +6 -0
- data/test/active_model_serializer_plus/active_model_serializer_plus_test.rb +10 -0
- data/test/active_model_serializer_plus/assignment_test.rb +64 -0
- data/test/active_model_serializer_plus/translations_test.rb +260 -0
- data/test/app/rails_4.2/Rakefile +6 -0
- data/test/app/rails_4.2/app/assets/javascripts/application.js +16 -0
- data/test/app/rails_4.2/app/assets/stylesheets/application.css +15 -0
- data/test/app/rails_4.2/app/controllers/application_controller.rb +5 -0
- data/test/app/rails_4.2/app/helpers/application_helper.rb +2 -0
- data/test/app/rails_4.2/app/views/layouts/application.html.erb +14 -0
- data/test/app/rails_4.2/bin/bundle +3 -0
- data/test/app/rails_4.2/bin/rails +8 -0
- data/test/app/rails_4.2/bin/rake +8 -0
- data/test/app/rails_4.2/bin/setup +29 -0
- data/test/app/rails_4.2/bin/spring +15 -0
- data/test/app/rails_4.2/config.ru +4 -0
- data/test/app/rails_4.2/config/application.rb +26 -0
- data/test/app/rails_4.2/config/boot.rb +3 -0
- data/test/app/rails_4.2/config/database.yml +25 -0
- data/test/app/rails_4.2/config/environment.rb +5 -0
- data/test/app/rails_4.2/config/environments/development.rb +41 -0
- data/test/app/rails_4.2/config/environments/production.rb +79 -0
- data/test/app/rails_4.2/config/environments/test.rb +42 -0
- data/test/app/rails_4.2/config/initializers/assets.rb +11 -0
- data/test/app/rails_4.2/config/initializers/backtrace_silencers.rb +7 -0
- data/test/app/rails_4.2/config/initializers/cookies_serializer.rb +3 -0
- data/test/app/rails_4.2/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/app/rails_4.2/config/initializers/inflections.rb +16 -0
- data/test/app/rails_4.2/config/initializers/mime_types.rb +4 -0
- data/test/app/rails_4.2/config/initializers/session_store.rb +3 -0
- data/test/app/rails_4.2/config/initializers/wrap_parameters.rb +14 -0
- data/test/app/rails_4.2/config/locales/en.yml +23 -0
- data/test/app/rails_4.2/config/routes.rb +56 -0
- data/test/app/rails_4.2/config/secrets.yml +22 -0
- data/test/app/rails_4.2/db/schema.rb +0 -0
- data/test/app/rails_4.2/db/seeds.rb +7 -0
- data/test/app/rails_4.2/public/404.html +67 -0
- data/test/app/rails_4.2/public/422.html +67 -0
- data/test/app/rails_4.2/public/500.html +66 -0
- data/test/app/rails_4.2/public/favicon.ico +0 -0
- data/test/app/rails_4.2/public/robots.txt +5 -0
- data/test/test_helper.rb +20 -0
- 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
data/Appraisals
ADDED
data/CHANGELOG.md
ADDED
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,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,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,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
|