hashcast 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'hashcast/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "hashcast"
8
+ spec.version = HashCast::VERSION
9
+ spec.authors = ["Albert Gazizov", "Roman Heinrich"]
10
+ spec.email = ["deeper4k@gmail.com"]
11
+ spec.description = %q{Declarative Hash Caster}
12
+ spec.summary = %q{Declarative Hash Caster}
13
+ spec.homepage = "http://github.com/ddd-ruby/hashcast"
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 "byebug", '~> 0'
23
+ spec.add_development_dependency "rake", '~> 0'
24
+ spec.add_development_dependency "bixby-bench", '~> 0'
25
+ spec.add_development_dependency "allocation_stats", '~> 0'
26
+ spec.add_development_dependency "codecov", '~> 0'
27
+ end
@@ -0,0 +1,46 @@
1
+ require 'hashcast/version'
2
+ require 'hashcast/errors'
3
+ require 'hashcast/config'
4
+ require 'hashcast/casters'
5
+ require 'hashcast/concern'
6
+ require 'hashcast/metadata/attribute'
7
+ require 'hashcast/attributes_parser'
8
+ require 'hashcast/attributes_caster'
9
+ require 'hashcast/caster'
10
+
11
+ module HashCast
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 HashCast
25
+ # Allow extend HashCast 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 ||= HashCast::Config.new
34
+ end
35
+ end
36
+
37
+ HashCast.add_caster(:array, HashCast::Casters::ArrayCaster)
38
+ HashCast.add_caster(:boolean, HashCast::Casters::BooleanCaster)
39
+ HashCast.add_caster(:date, HashCast::Casters::DateCaster)
40
+ HashCast.add_caster(:datetime, HashCast::Casters::DateTimeCaster)
41
+ HashCast.add_caster(:float, HashCast::Casters::FloatCaster)
42
+ HashCast.add_caster(:hash, HashCast::Casters::HashCast)
43
+ HashCast.add_caster(:integer, HashCast::Casters::IntegerCaster)
44
+ HashCast.add_caster(:string, HashCast::Casters::StringCaster)
45
+ HashCast.add_caster(:symbol, HashCast::Casters::SymbolCaster)
46
+ HashCast.add_caster(:time, HashCast::Casters::TimeCaster)
@@ -0,0 +1,97 @@
1
+ class HashCast::AttributesCaster
2
+ attr_reader :attributes, :options
3
+
4
+ def initialize(attributes, options)
5
+ @attributes = attributes
6
+ @options = options
7
+ end
8
+
9
+ def cast(input_hash)
10
+ casted_hash = {}
11
+
12
+ hash_keys = get_keys(input_hash)
13
+ attributes.each do |attribute|
14
+ if hash_keys.include?(attribute.name)
15
+ begin
16
+ casted_value = cast_attribute(attribute, input_hash)
17
+ casted_hash[cast_key(attribute.name, options)] = casted_value
18
+ rescue HashCast::Errors::AttributeError => e
19
+ e.add_namespace(attribute.name)
20
+ raise e
21
+ end
22
+ else
23
+ raise HashCast::Errors::MissingAttributeError.new("should be given", attribute.name) if attribute.required?
24
+ end
25
+ end
26
+
27
+ if !options[:skip_unexpected_attributes]
28
+ check_unexpected_attributes_not_given!(hash_keys, casted_hash.keys)
29
+ end
30
+
31
+ casted_hash
32
+ end
33
+
34
+ private
35
+
36
+ def cast_attribute(attribute, hash)
37
+ value = get_value(hash, attribute.name)
38
+ return nil if value.nil? && attribute.allow_nil?
39
+
40
+ casted_value = attribute.caster.cast(value, attribute.name, attribute.options)
41
+
42
+ if attribute.has_children?
43
+ return cast_children(casted_value, attribute)
44
+ end
45
+ if caster = attribute.options[:caster]
46
+ return cast_children_with_caster(casted_value, attribute, caster)
47
+ end
48
+
49
+ casted_value
50
+ end
51
+
52
+ def cast_children(value, attribute)
53
+ caster = self.class.new(attribute.children, options)
54
+ cast_children_with_caster(value, attribute, caster)
55
+ end
56
+
57
+ def cast_children_with_caster(value, attribute, caster)
58
+ return caster.cast(value) if attribute.caster != HashCast::Casters::ArrayCaster
59
+
60
+ value.map do |val|
61
+ caster.cast(val)
62
+ end
63
+ end
64
+
65
+ def cast_key(value, options)
66
+ return value if options[:output_keys] == :symbol
67
+ return value.to_s if options[:output_keys] == :string
68
+ end
69
+
70
+ def get_keys(hash)
71
+ return hash.keys if same_in_out_key_format?
72
+ hash.keys.map(&:to_sym)
73
+ end
74
+
75
+ def get_value(hash, key)
76
+ return hash[key] if same_in_out_key_format?
77
+ return hash[key.to_sym] if options[:input_keys] == :symbol
78
+ hash[key.to_s]
79
+ end
80
+
81
+ def check_unexpected_attributes_not_given!(input_hash_keys, casted_hash_keys)
82
+ unexpected_keys = keys_diff(input_hash_keys, casted_hash_keys)
83
+ unless unexpected_keys.empty?
84
+ raise HashCast::Errors::UnexpectedAttributeError.new("is not valid attribute name", unexpected_keys.first)
85
+ end
86
+ end
87
+
88
+ def same_in_out_key_format?
89
+ options[:input_keys] == options[:output_keys]
90
+ end
91
+
92
+ def keys_diff(input_hash_keys, casted_hash_keys)
93
+ return (input_hash_keys - casted_hash_keys) if same_in_out_key_format?
94
+ return (input_hash_keys - casted_hash_keys) if options[:output_keys] == :symbol # same for symbol
95
+ return (input_hash_keys - casted_hash_keys.map(&:to_sym)) if options[:output_keys] == :string
96
+ end
97
+ end
@@ -0,0 +1,63 @@
1
+ # Parses caster rules
2
+ # and returns list of HashCast::Metadata::Attribute instances
3
+ # which contains casting rules
4
+ class HashCast::AttributesParser
5
+
6
+ # Performs casting
7
+ # @param block [Proc] block with casting rules
8
+ # @return Array(HashCast::Metadata::Attribute) list of casting rules
9
+ def self.parse(&block)
10
+ dsl = DSL.new
11
+ dsl.instance_exec(&block)
12
+ dsl.attributes
13
+ end
14
+
15
+ class DSL
16
+ attr_reader :attributes
17
+
18
+ def initialize
19
+ @attributes = []
20
+ end
21
+
22
+ # Redefined becase each class has the built in hash method
23
+ def hash(*args, &block)
24
+ method_missing(:hash, *args, &block)
25
+ end
26
+
27
+ def method_missing(caster_name, *args, &block)
28
+ attr_name = args[0]
29
+ options = args[1] || {}
30
+ caster = HashCast.casters[caster_name]
31
+
32
+ check_caster_exists!(caster, caster_name)
33
+ check_attr_name_valid!(attr_name)
34
+ check_options_is_hash!(options)
35
+
36
+ attribute = HashCast::Metadata::Attribute.new(attr_name, caster, options)
37
+ if block_given?
38
+ attribute.children = HashCast::AttributesParser.parse(&block)
39
+ end
40
+ attributes << attribute
41
+ end
42
+
43
+ private
44
+
45
+ def check_caster_exists!(caster, caster_name)
46
+ if !caster
47
+ raise HashCast::Errors::CasterNotFoundError, "caster with name '#{caster_name}' is not found"
48
+ end
49
+ end
50
+
51
+ def check_attr_name_valid!(attr_name)
52
+ if !attr_name.is_a?(Symbol) && !attr_name.is_a?(String)
53
+ raise HashCast::Errors::ArgumentError, "attribute name should be a symbol or string"
54
+ end
55
+ end
56
+
57
+ def check_options_is_hash!(options)
58
+ if !options.is_a?(Hash)
59
+ raise HashCast::Errors::ArgumentError, "attribute options should be a Hash"
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,144 @@
1
+ # Include this module to create your caster
2
+ #
3
+ # Example caster:
4
+ # class ContactCaster
5
+ # include HashCast::Caster
6
+ #
7
+ # attributes do
8
+ # hash :contact do
9
+ # string :name
10
+ # integer :age, optional: true
11
+ # float :weight
12
+ # date :birthday
13
+ # datetime :last_logged_in
14
+ # time :last_visited_at
15
+ # hash :company do
16
+ # string :name
17
+ # end
18
+ # array :emails, each: :string
19
+ # array :social_accounts, each: :hash do
20
+ # string :name
21
+ # symbol :type
22
+ # end
23
+ # end
24
+ # end
25
+ # end
26
+ #
27
+ # The defined caster will have #cast method which accepts hash
28
+ # Use it to cast hash:
29
+ # ContactCaster.new.cast({
30
+ # contact: {
31
+ # name: "John Smith",
32
+ # age: "22",
33
+ # weight: "65.5",
34
+ # birthday: "2014-02-02",
35
+ # last_logged_in: "2014-02-02 10:10:00",
36
+ # last_visited_at: "2014-02-02 10:10:00",
37
+ # company: {
38
+ # name: "MyCo",
39
+ # },
40
+ # emails: [ "test@example.com", "test2@example.com" ],
41
+ # social_accounts: [
42
+ # {
43
+ # name: "john_smith",
44
+ # type: 'twitter',
45
+ # },
46
+ # {
47
+ # name: "John",
48
+ # type: :facebook,
49
+ # },
50
+ # ]
51
+ # }
52
+ # })
53
+ #
54
+ # The output will be casted hash:
55
+ # {
56
+ # contact: {
57
+ # name: "John Smith",
58
+ # age: 22,
59
+ # weight: 65.5,
60
+ # birthday: Date.parse("2014-02-02"),
61
+ # last_logged_in: DateTime.parse("2014-02-02 10:10:00"),
62
+ # last_visited_at: Time.parse("2014-02-02 10:10:00"),
63
+ # company: {
64
+ # name: "MyCo",
65
+ # },
66
+ # emails: [ "test@example.com", "test2@example.com" ],
67
+ # social_accounts: [
68
+ # {
69
+ # name: "john_smith",
70
+ # type: :twitter,
71
+ # },
72
+ # {
73
+ # name: "John",
74
+ # type: :facebook,
75
+ # },
76
+ # ]
77
+ # }
78
+ # }
79
+ module HashCast::Caster
80
+ extend HashCast::Concern
81
+
82
+ module ClassMethods
83
+ ALLOWED_OPTIONS = [:string, :symbol]
84
+
85
+ # Defines casting rules
86
+ # @example
87
+ # attributes do
88
+ # string :first_name
89
+ # string :last_name
90
+ # integer :age, optional: true
91
+ # end
92
+ def attributes(&block)
93
+ raise ArgumentError, "You should provide block" unless block_given?
94
+
95
+ attributes = HashCast::AttributesParser.parse(&block)
96
+ self.instance_variable_set(:@attributes, attributes)
97
+ end
98
+
99
+ # Performs casting
100
+ # @param hash [Hash] hash for casting
101
+ # @param options [Hash] options, input_keys: :string, output_key: :symbol
102
+ def cast(hash, options = {})
103
+ check_attributes_defined!
104
+ check_hash_given!(hash)
105
+ check_options!(options)
106
+ options = set_default_options(options)
107
+
108
+ attributes_caster = HashCast::AttributesCaster.new(instance_variable_get(:@attributes), options)
109
+ attributes_caster.cast(hash)
110
+ end
111
+
112
+ private
113
+
114
+ def check_attributes_defined!
115
+ unless instance_variable_defined?(:@attributes)
116
+ raise HashCast::Errors::ArgumentError, "Attributes block should be defined"
117
+ end
118
+ end
119
+
120
+ def check_options!(options)
121
+ unless options.is_a?(Hash)
122
+ raise HashCast::Errors::ArgumentError, "Options should be a hash"
123
+ end
124
+ if options[:input_keys] && !ALLOWED_OPTIONS.include?(options[:input_keys])
125
+ raise HashCast::Errors::ArgumentError, "input_keys should be :string or :symbol"
126
+ end
127
+ if options[:output_keys] && !ALLOWED_OPTIONS.include?(options[:output_keys])
128
+ raise HashCast::Errors::ArgumentError, "output_keys should be :string or :symbol"
129
+ end
130
+ end
131
+
132
+ def check_hash_given!(hash)
133
+ unless hash.is_a?(Hash)
134
+ raise HashCast::Errors::ArgumentError, "Hash should be given"
135
+ end
136
+ end
137
+
138
+ def set_default_options(options)
139
+ options[:input_keys] ||= HashCast.config.input_keys
140
+ options[:output_keys] ||= HashCast.config.output_keys
141
+ options
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,13 @@
1
+ # List of build in casters
2
+ module HashCast::Casters
3
+ require 'hashcast/casters/array_caster'
4
+ require 'hashcast/casters/boolean_caster'
5
+ require 'hashcast/casters/date_caster'
6
+ require 'hashcast/casters/datetime_caster'
7
+ require 'hashcast/casters/float_caster'
8
+ require 'hashcast/casters/hash_caster'
9
+ require 'hashcast/casters/integer_caster'
10
+ require 'hashcast/casters/string_caster'
11
+ require 'hashcast/casters/symbol_caster'
12
+ require 'hashcast/casters/time_caster'
13
+ end
@@ -0,0 +1,27 @@
1
+ class HashCast::Casters::ArrayCaster
2
+
3
+ def self.cast(value, attr_name, options = {})
4
+ raise HashCast::Errors::CastingError, "should be an array" unless value.is_a?(Array)
5
+ return value unless options[:each]
6
+
7
+ cast_array_items(value, attr_name, options)
8
+ end
9
+
10
+ private
11
+
12
+ def self.cast_array_items(array, attr_name, options)
13
+ caster_name = options[:each]
14
+ caster = HashCast.casters[caster_name]
15
+ check_caster_exists!(caster, caster_name)
16
+ array.map do |item|
17
+ caster.cast(item, "#{attr_name} item", options)
18
+ end
19
+ end
20
+
21
+ def self.check_caster_exists!(caster, caster_name)
22
+ unless caster
23
+ raise HashCast::Errors::CasterNotFoundError, "caster with name #{caster_name} is not found"
24
+ end
25
+ end
26
+
27
+ end
@@ -0,0 +1,13 @@
1
+ class HashCast::Casters::BooleanCaster
2
+ REAL_BOOLEANS = [TrueClass, FalseClass]
3
+ TRUE_VALUES = ['1', 'true', 'on', 1]
4
+ FALSE_VALUES = ['0', 'false', 'off', 0]
5
+
6
+ def self.cast(value, attr_name, options = {})
7
+ return value if REAL_BOOLEANS.include?(value.class)
8
+ return true if TRUE_VALUES.include?(value)
9
+ return false if FALSE_VALUES.include?(value)
10
+ raise HashCast::Errors::CastingError, "should be a boolean"
11
+ end
12
+
13
+ end
@@ -0,0 +1,15 @@
1
+ class HashCast::Casters::DateCaster
2
+
3
+ def self.cast(value, attr_name, options = {})
4
+ return value if value.is_a?(Date)
5
+ return cast_string(value) if value.is_a?(String)
6
+ raise HashCast::Errors::CastingError, "should be a date"
7
+ end
8
+
9
+ def self.cast_string(value)
10
+ Date.parse(value)
11
+ rescue ArgumentError => e
12
+ raise HashCast::Errors::CastingError, "is invalid date"
13
+ end
14
+
15
+ end