morf 0.0.1
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.
- 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
|