morf 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/.travis.yml +3 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +37 -0
- data/LICENSE.txt +22 -0
- data/README.md +1 -0
- data/Rakefile +1 -0
- data/lib/morf.rb +46 -0
- data/lib/morf/attributes_caster.rb +98 -0
- data/lib/morf/attributes_parser.rb +63 -0
- data/lib/morf/caster.rb +142 -0
- data/lib/morf/casters.rb +13 -0
- data/lib/morf/casters/array_caster.rb +30 -0
- data/lib/morf/casters/boolean_caster.rb +13 -0
- data/lib/morf/casters/date_caster.rb +17 -0
- data/lib/morf/casters/datetime_caster.rb +17 -0
- data/lib/morf/casters/float_caster.rb +15 -0
- data/lib/morf/casters/hash_caster.rb +9 -0
- data/lib/morf/casters/integer_caster.rb +15 -0
- data/lib/morf/casters/string_caster.rb +11 -0
- data/lib/morf/casters/symbol_caster.rb +17 -0
- data/lib/morf/casters/time_caster.rb +17 -0
- data/lib/morf/concern.rb +136 -0
- data/lib/morf/config.rb +11 -0
- data/lib/morf/errors.rb +67 -0
- data/lib/morf/metadata/attribute.rb +30 -0
- data/lib/morf/version.rb +3 -0
- data/morf.gemspec +24 -0
- data/spec/morf/caster_spec.rb +384 -0
- data/spec/morf/morf_spec.rb +37 -0
- data/spec/spec_helper.rb +6 -0
- metadata +120 -0
data/lib/morf/casters.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# List of build in casters
|
2
|
+
module Morf::Casters
|
3
|
+
require 'morf/casters/array_caster'
|
4
|
+
require 'morf/casters/boolean_caster'
|
5
|
+
require 'morf/casters/date_caster'
|
6
|
+
require 'morf/casters/datetime_caster'
|
7
|
+
require 'morf/casters/float_caster'
|
8
|
+
require 'morf/casters/hash_caster'
|
9
|
+
require 'morf/casters/integer_caster'
|
10
|
+
require 'morf/casters/string_caster'
|
11
|
+
require 'morf/casters/symbol_caster'
|
12
|
+
require 'morf/casters/time_caster'
|
13
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class Morf::Casters::ArrayCaster
|
2
|
+
def self.cast(value, attr_name, options = {})
|
3
|
+
if value.is_a?(Array)
|
4
|
+
if options[:each]
|
5
|
+
cast_array_items(value, attr_name, options)
|
6
|
+
else
|
7
|
+
value
|
8
|
+
end
|
9
|
+
else
|
10
|
+
raise Morf::Errors::CastingError, "should be an array"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def self.cast_array_items(array, attr_name, options)
|
17
|
+
caster_name = options[:each]
|
18
|
+
caster = Morf.casters[caster_name]
|
19
|
+
check_caster_exists!(caster, caster_name)
|
20
|
+
array.map do |item|
|
21
|
+
caster.cast(item, "#{attr_name} item", options)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.check_caster_exists!(caster, caster_name)
|
26
|
+
unless caster
|
27
|
+
raise Morf::Errors::CasterNotFoundError, "caster with name #{caster_name} is not found"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class Morf::Casters::BooleanCaster
|
2
|
+
def self.cast(value, attr_name, options = {})
|
3
|
+
if [TrueClass, FalseClass].include?(value.class)
|
4
|
+
value
|
5
|
+
elsif ['1', 'true', 'on', 1].include?(value)
|
6
|
+
true
|
7
|
+
elsif ['0', 'false', 'off', 0].include?(value)
|
8
|
+
false
|
9
|
+
else
|
10
|
+
raise Morf::Errors::CastingError, "should be a boolean"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'active_support/core_ext/date'
|
2
|
+
|
3
|
+
class Morf::Casters::DateCaster
|
4
|
+
def self.cast(value, attr_name, options = {})
|
5
|
+
if value.is_a?(Date)
|
6
|
+
value
|
7
|
+
elsif value.is_a?(String)
|
8
|
+
begin
|
9
|
+
Date.parse(value)
|
10
|
+
rescue ArgumentError => e
|
11
|
+
raise Morf::Errors::CastingError, "is invalid date"
|
12
|
+
end
|
13
|
+
else
|
14
|
+
raise Morf::Errors::CastingError, "should be a date"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class Morf::Casters::DateTimeCaster
|
2
|
+
def self.cast(value, attr_name, options = {})
|
3
|
+
if value.is_a?(DateTime)
|
4
|
+
value
|
5
|
+
elsif value.is_a?(Time)
|
6
|
+
value.to_datetime
|
7
|
+
elsif value.is_a?(String)
|
8
|
+
begin
|
9
|
+
DateTime.parse(value)
|
10
|
+
rescue ArgumentError => e
|
11
|
+
raise Morf::Errors::CastingError, "is invalid datetime"
|
12
|
+
end
|
13
|
+
else
|
14
|
+
raise Morf::Errors::CastingError, "should be a datetime"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class Morf::Casters::FloatCaster
|
2
|
+
def self.cast(value, attr_name, options = {})
|
3
|
+
if value.is_a?(Float)
|
4
|
+
value
|
5
|
+
elsif value.is_a?(String)
|
6
|
+
begin
|
7
|
+
Float(value)
|
8
|
+
rescue ArgumentError => e
|
9
|
+
raise Morf::Errors::CastingError, "is invalid float"
|
10
|
+
end
|
11
|
+
else
|
12
|
+
raise Morf::Errors::CastingError, "should be a float"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class Morf::Casters::IntegerCaster
|
2
|
+
def self.cast(value, attr_name, options = {})
|
3
|
+
if value.is_a?(Integer)
|
4
|
+
value
|
5
|
+
elsif value.is_a?(String)
|
6
|
+
begin
|
7
|
+
Integer(value)
|
8
|
+
rescue ArgumentError => e
|
9
|
+
raise Morf::Errors::CastingError, "is invalid integer"
|
10
|
+
end
|
11
|
+
else
|
12
|
+
raise Morf::Errors::CastingError, "should be a integer"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class Morf::Casters::SymbolCaster
|
2
|
+
MAX_SYMBOL_LENGTH = 1000
|
3
|
+
|
4
|
+
def self.cast(value, attr_name, options = {})
|
5
|
+
if value.is_a?(Symbol)
|
6
|
+
value
|
7
|
+
elsif value.is_a?(String)
|
8
|
+
if value.length > MAX_SYMBOL_LENGTH
|
9
|
+
raise Morf::Errors::CastingError, "is too long to be a symbol"
|
10
|
+
else
|
11
|
+
value.to_sym
|
12
|
+
end
|
13
|
+
else
|
14
|
+
raise Morf::Errors::CastingError, "should be a symbol"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
class Morf::Casters::TimeCaster
|
4
|
+
def self.cast(value, attr_name, options = {})
|
5
|
+
if value.is_a?(Time)
|
6
|
+
value
|
7
|
+
elsif value.is_a?(String)
|
8
|
+
begin
|
9
|
+
Time.parse(value)
|
10
|
+
rescue ArgumentError => e
|
11
|
+
raise Morf::Errors::CastingError, "is invalid time"
|
12
|
+
end
|
13
|
+
else
|
14
|
+
raise Morf::Errors::CastingError, "should be a time"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/morf/concern.rb
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
module Morf
|
2
|
+
# Copied from here https://github.com/rails/rails/blob/master/activesupport/lib/active_support/concern.rb
|
3
|
+
#
|
4
|
+
# A typical module looks like this:
|
5
|
+
#
|
6
|
+
# module M
|
7
|
+
# def self.included(base)
|
8
|
+
# base.extend ClassMethods
|
9
|
+
# base.class_eval do
|
10
|
+
# scope :disabled, -> { where(disabled: true) }
|
11
|
+
# end
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# module ClassMethods
|
15
|
+
# ...
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# By using <tt>ActiveSupport::Concern</tt> the above module could instead be
|
20
|
+
# written as:
|
21
|
+
#
|
22
|
+
# require 'active_support/concern'
|
23
|
+
#
|
24
|
+
# module M
|
25
|
+
# extend ActiveSupport::Concern
|
26
|
+
#
|
27
|
+
# included do
|
28
|
+
# scope :disabled, -> { where(disabled: true) }
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# module ClassMethods
|
32
|
+
# ...
|
33
|
+
# end
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# Moreover, it gracefully handles module dependencies. Given a +Foo+ module
|
37
|
+
# and a +Bar+ module which depends on the former, we would typically write the
|
38
|
+
# following:
|
39
|
+
#
|
40
|
+
# module Foo
|
41
|
+
# def self.included(base)
|
42
|
+
# base.class_eval do
|
43
|
+
# def self.method_injected_by_foo
|
44
|
+
# ...
|
45
|
+
# end
|
46
|
+
# end
|
47
|
+
# end
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# module Bar
|
51
|
+
# def self.included(base)
|
52
|
+
# base.method_injected_by_foo
|
53
|
+
# end
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# class Host
|
57
|
+
# include Foo # We need to include this dependency for Bar
|
58
|
+
# include Bar # Bar is the module that Host really needs
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# But why should +Host+ care about +Bar+'s dependencies, namely +Foo+? We
|
62
|
+
# could try to hide these from +Host+ directly including +Foo+ in +Bar+:
|
63
|
+
#
|
64
|
+
# module Bar
|
65
|
+
# include Foo
|
66
|
+
# def self.included(base)
|
67
|
+
# base.method_injected_by_foo
|
68
|
+
# end
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
# class Host
|
72
|
+
# include Bar
|
73
|
+
# end
|
74
|
+
#
|
75
|
+
# Unfortunately this won't work, since when +Foo+ is included, its <tt>base</tt>
|
76
|
+
# is the +Bar+ module, not the +Host+ class. With <tt>ActiveSupport::Concern</tt>,
|
77
|
+
# module dependencies are properly resolved:
|
78
|
+
#
|
79
|
+
# require 'active_support/concern'
|
80
|
+
#
|
81
|
+
# module Foo
|
82
|
+
# extend ActiveSupport::Concern
|
83
|
+
# included do
|
84
|
+
# def self.method_injected_by_foo
|
85
|
+
# ...
|
86
|
+
# end
|
87
|
+
# end
|
88
|
+
# end
|
89
|
+
#
|
90
|
+
# module Bar
|
91
|
+
# extend ActiveSupport::Concern
|
92
|
+
# include Foo
|
93
|
+
#
|
94
|
+
# included do
|
95
|
+
# self.method_injected_by_foo
|
96
|
+
# end
|
97
|
+
# end
|
98
|
+
#
|
99
|
+
# class Host
|
100
|
+
# include Bar # works, Bar takes care now of its dependencies
|
101
|
+
# end
|
102
|
+
module Concern
|
103
|
+
class MultipleIncludedBlocks < StandardError #:nodoc:
|
104
|
+
def initialize
|
105
|
+
super "Cannot define multiple 'included' blocks for a Concern"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.extended(base) #:nodoc:
|
110
|
+
base.instance_variable_set(:@_dependencies, [])
|
111
|
+
end
|
112
|
+
|
113
|
+
def append_features(base)
|
114
|
+
if base.instance_variable_defined?(:@_dependencies)
|
115
|
+
base.instance_variable_get(:@_dependencies) << self
|
116
|
+
return false
|
117
|
+
else
|
118
|
+
return false if base < self
|
119
|
+
@_dependencies.each { |dep| base.send(:include, dep) }
|
120
|
+
super
|
121
|
+
base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
|
122
|
+
base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def included(base = nil, &block)
|
127
|
+
if base.nil?
|
128
|
+
raise MultipleIncludedBlocks if instance_variable_defined?(:@_included_block)
|
129
|
+
|
130
|
+
@_included_block = block
|
131
|
+
else
|
132
|
+
super
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
data/lib/morf/config.rb
ADDED
data/lib/morf/errors.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
module Morf::Errors
|
2
|
+
|
3
|
+
# Base error class for all Morf errors
|
4
|
+
class MorfError < StandardError; end
|
5
|
+
|
6
|
+
# Raised when caster with given name is not registered in Morf
|
7
|
+
class CasterNotFoundError < MorfError; end
|
8
|
+
|
9
|
+
# Raised when some of the given to Morf argument is not valid
|
10
|
+
class ArgumentError < MorfError; end
|
11
|
+
|
12
|
+
class AttributeError < MorfError
|
13
|
+
attr_reader :namespaces
|
14
|
+
|
15
|
+
def initialize(message, namespace = nil)
|
16
|
+
super(message)
|
17
|
+
@namespaces = []
|
18
|
+
@namespaces << namespace if namespace
|
19
|
+
end
|
20
|
+
|
21
|
+
def add_namespace(namespace)
|
22
|
+
namespaces << namespace
|
23
|
+
end
|
24
|
+
|
25
|
+
def message
|
26
|
+
to_s
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_s
|
30
|
+
if namespaces.empty?
|
31
|
+
super
|
32
|
+
else
|
33
|
+
reverted_namespaces = namespaces.reverse
|
34
|
+
msg = reverted_namespaces.first.to_s
|
35
|
+
msg += reverted_namespaces[1..-1].inject("") { |res, item| res += "[#{item}]"}
|
36
|
+
msg + " " + super
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
# Raised when hash attribute can't be casted
|
42
|
+
class CastingError < AttributeError; end
|
43
|
+
|
44
|
+
# Raised when required hash attribute wasn't given for casting
|
45
|
+
class MissingAttributeError < AttributeError; end
|
46
|
+
|
47
|
+
# Raised when unexpected hash attribute was given for casting
|
48
|
+
class UnexpectedAttributeError < AttributeError; end
|
49
|
+
|
50
|
+
# Raised when hash has validation errors
|
51
|
+
class ValidationError < StandardError
|
52
|
+
attr_reader :errors
|
53
|
+
|
54
|
+
def initialize(message, errors)
|
55
|
+
@errors = errors
|
56
|
+
super(message)
|
57
|
+
end
|
58
|
+
|
59
|
+
def message
|
60
|
+
"#{@message}\n#{errors.to_hash}"
|
61
|
+
end
|
62
|
+
|
63
|
+
def short_message
|
64
|
+
'Validation error'
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Morf::Metadata
|
2
|
+
class Attribute
|
3
|
+
attr_reader :name, :caster, :options
|
4
|
+
attr_accessor :children
|
5
|
+
|
6
|
+
def initialize(name, caster, options)
|
7
|
+
@name = name
|
8
|
+
@caster = caster
|
9
|
+
@options = options
|
10
|
+
@children = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def has_children?
|
14
|
+
!children.empty?
|
15
|
+
end
|
16
|
+
|
17
|
+
def required?
|
18
|
+
!optional?
|
19
|
+
end
|
20
|
+
|
21
|
+
def optional?
|
22
|
+
!!options[:optional]
|
23
|
+
end
|
24
|
+
|
25
|
+
def allow_nil?
|
26
|
+
!!options[:allow_nil]
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
data/lib/morf/version.rb
ADDED
data/morf.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'morf/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "morf"
|
8
|
+
spec.version = Morf::VERSION
|
9
|
+
spec.authors = ["Droid Labs LLC"]
|
10
|
+
spec.email = ["droid@droidlabs.pro"]
|
11
|
+
spec.description = %q{Declarative object morpher}
|
12
|
+
spec.summary = %q{Declarative object morher}
|
13
|
+
spec.homepage = "https://github.com/droidlabs/morf"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(spec)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "activesupport", "~> 1.3"
|
23
|
+
spec.add_development_dependency "rake"
|
24
|
+
end
|