hash_cast 0.4.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.
- checksums.yaml +7 -0
- data/.github/workflows/rspec.yml +34 -0
- data/.gitignore +6 -0
- data/.rspec +1 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +35 -0
- data/LICENSE.txt +22 -0
- data/README.md +93 -0
- data/Rakefile +1 -0
- data/hcast.gemspec +23 -0
- data/lib/hcast/attributes_caster.rb +102 -0
- data/lib/hcast/attributes_parser.rb +63 -0
- data/lib/hcast/caster.rb +142 -0
- data/lib/hcast/casters/array_caster.rb +32 -0
- data/lib/hcast/casters/boolean_caster.rb +15 -0
- data/lib/hcast/casters/date_caster.rb +19 -0
- data/lib/hcast/casters/datetime_caster.rb +19 -0
- data/lib/hcast/casters/float_caster.rb +17 -0
- data/lib/hcast/casters/hash_caster.rb +11 -0
- data/lib/hcast/casters/integer_caster.rb +17 -0
- data/lib/hcast/casters/string_caster.rb +13 -0
- data/lib/hcast/casters/symbol_caster.rb +18 -0
- data/lib/hcast/casters/time_caster.rb +19 -0
- data/lib/hcast/casters.rb +13 -0
- data/lib/hcast/concern.rb +136 -0
- data/lib/hcast/config.rb +11 -0
- data/lib/hcast/errors.rb +67 -0
- data/lib/hcast/metadata/attribute.rb +30 -0
- data/lib/hcast/version.rb +3 -0
- data/lib/hcast.rb +46 -0
- data/spec/hcast/caster_spec.rb +384 -0
- data/spec/hcast/hcast_spec.rb +37 -0
- data/spec/spec_helper.rb +6 -0
- metadata +108 -0
@@ -0,0 +1,18 @@
|
|
1
|
+
class HCast::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 HCast::Errors::CastingError, "is too long to be a symbol"
|
10
|
+
else
|
11
|
+
value.to_sym
|
12
|
+
end
|
13
|
+
else
|
14
|
+
raise HCast::Errors::CastingError, "should be a symbol"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
class HCast::Casters::TimeCaster
|
4
|
+
|
5
|
+
def self.cast(value, attr_name, options = {})
|
6
|
+
if value.is_a?(Time)
|
7
|
+
value
|
8
|
+
elsif value.is_a?(String)
|
9
|
+
begin
|
10
|
+
Time.parse(value)
|
11
|
+
rescue ArgumentError => e
|
12
|
+
raise HCast::Errors::CastingError, "is invalid time"
|
13
|
+
end
|
14
|
+
else
|
15
|
+
raise HCast::Errors::CastingError, "should be a time"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# List of build in casters
|
2
|
+
module HCast::Casters
|
3
|
+
require 'hcast/casters/array_caster'
|
4
|
+
require 'hcast/casters/boolean_caster'
|
5
|
+
require 'hcast/casters/date_caster'
|
6
|
+
require 'hcast/casters/datetime_caster'
|
7
|
+
require 'hcast/casters/float_caster'
|
8
|
+
require 'hcast/casters/hash_caster'
|
9
|
+
require 'hcast/casters/integer_caster'
|
10
|
+
require 'hcast/casters/string_caster'
|
11
|
+
require 'hcast/casters/symbol_caster'
|
12
|
+
require 'hcast/casters/time_caster'
|
13
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
module HCast
|
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/hcast/config.rb
ADDED
data/lib/hcast/errors.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
module HCast::Errors
|
2
|
+
|
3
|
+
# Base error class for all HCast errors
|
4
|
+
class HCastError < StandardError; end
|
5
|
+
|
6
|
+
# Raised when caster with given name is not registered in HCast
|
7
|
+
class CasterNotFoundError < HCastError; end
|
8
|
+
|
9
|
+
# Raised when some of the given to HCast argument is not valid
|
10
|
+
class ArgumentError < HCastError; end
|
11
|
+
|
12
|
+
class AttributeError < HCastError
|
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 HCast::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/hcast.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'hcast/version'
|
2
|
+
require 'hcast/errors'
|
3
|
+
require 'hcast/config'
|
4
|
+
require 'hcast/casters'
|
5
|
+
require 'hcast/concern.rb'
|
6
|
+
require 'hcast/metadata/attribute'
|
7
|
+
require 'hcast/attributes_parser'
|
8
|
+
require 'hcast/attributes_caster'
|
9
|
+
require 'hcast/caster'
|
10
|
+
|
11
|
+
module HCast
|
12
|
+
@@casters = {}
|
13
|
+
|
14
|
+
# Defines caster without adding own class
|
15
|
+
# @note Not yet implemented
|
16
|
+
def self.create(&block)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns list of defined casters
|
20
|
+
def self.casters
|
21
|
+
@@casters
|
22
|
+
end
|
23
|
+
|
24
|
+
# Adds new casters to HCast
|
25
|
+
# Allow extend HCast with your own casters
|
26
|
+
# @param caster_name [Symbol] caster name
|
27
|
+
# @param caster [Class] caster
|
28
|
+
def self.add_caster(caster_name, caster)
|
29
|
+
@@casters[caster_name] = caster
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.config
|
33
|
+
@@config ||= HCast::Config.new
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
HCast.add_caster(:array, HCast::Casters::ArrayCaster)
|
38
|
+
HCast.add_caster(:boolean, HCast::Casters::BooleanCaster)
|
39
|
+
HCast.add_caster(:date, HCast::Casters::DateCaster)
|
40
|
+
HCast.add_caster(:datetime, HCast::Casters::DateTimeCaster)
|
41
|
+
HCast.add_caster(:float, HCast::Casters::FloatCaster)
|
42
|
+
HCast.add_caster(:hash, HCast::Casters::HashCaster)
|
43
|
+
HCast.add_caster(:integer, HCast::Casters::IntegerCaster)
|
44
|
+
HCast.add_caster(:string, HCast::Casters::StringCaster)
|
45
|
+
HCast.add_caster(:symbol, HCast::Casters::SymbolCaster)
|
46
|
+
HCast.add_caster(:time, HCast::Casters::TimeCaster)
|