hash_cast 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 71b3bcf7f98727c99228d5fb0ceaba02b87767721c251ba69d5c91079f936b5a
4
+ data.tar.gz: 1cd37fcce0009d5895dc72a4af32bd0c2dd2ea6ce5abee97b7e6b190a33c1655
5
+ SHA512:
6
+ metadata.gz: e607f921e463af6ff705ad3cfd86c9ca5532d7813913f6c795bfec73879b02cc4a8101b44ab9f475f45576ccbadd8b47d5d71e57ca3565b98813e4a3e5c6427c
7
+ data.tar.gz: a7297e7a81c4165dcda83135131e68817e29c043fd20594f5ce3ec9ab8b37eef07afd16bd9d606bf2a6ab0a8bb041e0cf17067ddc0b46212749e56b2a55afe34
@@ -0,0 +1,34 @@
1
+ # This workflow uses actions that are not certified by GitHub.
2
+ # They are provided by a third-party and are governed by
3
+ # separate terms of service, privacy policy, and support
4
+ # documentation.
5
+ # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
6
+ # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
7
+
8
+ name: Rspec
9
+
10
+ on:
11
+ push:
12
+ branches: [ master ]
13
+ pull_request:
14
+ branches: [ master ]
15
+
16
+ jobs:
17
+ test:
18
+
19
+ runs-on: ubuntu-latest
20
+ strategy:
21
+ matrix:
22
+ ruby-version: ['3.1', '3.3']
23
+
24
+ steps:
25
+ - uses: actions/checkout@v2
26
+ - name: Set up Ruby
27
+ # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
28
+ # change this to (see https://github.com/ruby/setup-ruby#versioning):
29
+ uses: ruby/setup-ruby@v1
30
+ with:
31
+ ruby-version: ${{ matrix.ruby-version }}
32
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
33
+ - name: Run tests
34
+ run: bundle exec rspec spec/
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ vendor/
2
+ tags
3
+ .bundle
4
+ .DS_Store
5
+ tmp/
6
+ pkg
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ hash_caster
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.3.4
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :test do
6
+ gem 'rspec'
7
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,35 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ hash_cast (0.4.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ diff-lcs (1.5.1)
10
+ rake (10.1.1)
11
+ rspec (3.13.0)
12
+ rspec-core (~> 3.13.0)
13
+ rspec-expectations (~> 3.13.0)
14
+ rspec-mocks (~> 3.13.0)
15
+ rspec-core (3.13.2)
16
+ rspec-support (~> 3.13.0)
17
+ rspec-expectations (3.13.3)
18
+ diff-lcs (>= 1.2.0, < 2.0)
19
+ rspec-support (~> 3.13.0)
20
+ rspec-mocks (3.13.2)
21
+ diff-lcs (>= 1.2.0, < 2.0)
22
+ rspec-support (~> 3.13.0)
23
+ rspec-support (3.13.1)
24
+
25
+ PLATFORMS
26
+ ruby
27
+
28
+ DEPENDENCIES
29
+ bundler
30
+ hash_cast!
31
+ rake
32
+ rspec
33
+
34
+ BUNDLED WITH
35
+ 2.5.22
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Albert Gazizov
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,93 @@
1
+ # HCast
2
+ [![Rspec](https://github.com/droidlabs/hcast/workflows/Rspec/badge.svg)](https://github.com/droidlabs/hcast/actions?query=workflow%3ARspec)
3
+
4
+ HCast is a library for casting hash attributes
5
+
6
+ ### Usage
7
+
8
+ Create caster class and declare hash attributes inside:
9
+ ```ruby
10
+ class ContactCaster
11
+ include HCast::Caster
12
+
13
+ attributes do
14
+ hash :contact do
15
+ string :name
16
+ integer :age, optional: true
17
+ float :weight
18
+ date :birthday
19
+ datetime :last_logged_in
20
+ time :last_visited_at
21
+ hash :company do
22
+ string :name
23
+ end
24
+ array :emails, each: :string
25
+ array :social_accounts, each: :hash do
26
+ string :name
27
+ symbol :type
28
+ end
29
+ end
30
+ end
31
+ end
32
+ ```
33
+ Instanticate the caster and give your hash for casting:
34
+ ```ruby
35
+ ContactCaster.cast({
36
+ contact: {
37
+ name: "John Smith",
38
+ age: "22",
39
+ weight: "65.5",
40
+ birthday: "2014-02-02",
41
+ last_logged_in: "2014-02-02 10:10:00",
42
+ last_visited_at: "2014-02-02 10:10:00",
43
+ company: {
44
+ name: "MyCo"
45
+ },
46
+ emails: ["test@example.com", "test2@example.com"],
47
+ social_accounts: [
48
+ {
49
+ name: "john_smith",
50
+ type: "twitter"
51
+ },
52
+ {
53
+ name: "John",
54
+ type: :facebook
55
+ }
56
+ ]
57
+ }
58
+ }
59
+ })
60
+ ```
61
+ The caster will cast your hash attributes to:
62
+ ```ruby
63
+ {
64
+ contact: {
65
+ name: "John Smith",
66
+ age: 22,
67
+ weight: 65.5,
68
+ birthday: #<Date: 2014-02-02 ((2456691j,0s,0n),+0s,2299161j)>,
69
+ last_logged_in: #<DateTime: 2014-02-02T10:10:00+00:00 ((2456691j,36600s,0n),+0s,2299161j)>,
70
+ last_visited_at: 2014-02-02 10:10:00 +0400,
71
+ company: {
72
+ name: "MyCo"
73
+ },
74
+ emails: ["test@example.com", "test2@example.com"],
75
+ social_accounts: [
76
+ {
77
+ name: "john_smith",
78
+ type: :twitter"
79
+ },
80
+ {
81
+ name: "John",
82
+ type: :facebook
83
+ }
84
+ ]
85
+ }
86
+ }
87
+ ```
88
+
89
+ if some of the attributes can't be casted the HCast::Errors::CastingError is raised
90
+
91
+
92
+ ## Author
93
+ Albert Gazizov, [@deeper4k](https://twitter.com/deeper4k)
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/hcast.gemspec ADDED
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'hcast/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "hash_cast"
8
+ spec.version = HCast::VERSION
9
+ spec.authors = ["Albert Gazizov"]
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/droidlabs/hash_cast"
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"
22
+ spec.add_development_dependency "rake"
23
+ end
@@ -0,0 +1,102 @@
1
+ class HCast::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, opts = {})
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[attribute.name] = casted_value
18
+ rescue HCast::Errors::AttributeError => e
19
+ handle_attribute_error(e, attribute)
20
+ end
21
+ else
22
+ raise HCast::Errors::MissingAttributeError.new("should be given", attribute.name)if attribute.required?
23
+ end
24
+ end
25
+
26
+ if !options[:skip_unexpected_attributes]
27
+ check_unexpected_attributes_not_given!(hash_keys, casted_hash.keys)
28
+ end
29
+
30
+ casted_hash
31
+ end
32
+
33
+ private
34
+
35
+ def handle_attribute_error(exception, attribute)
36
+ exception.add_namespace(attribute.name)
37
+ raise exception
38
+ end
39
+
40
+ def cast_attribute(attribute, hash)
41
+ value = get_value(hash, attribute.name)
42
+ if value.nil? && attribute.allow_nil?
43
+ nil
44
+ else
45
+ casted_value = attribute.caster.cast(value, attribute.name, attribute.options)
46
+ if attribute.has_children?
47
+ cast_children(casted_value, attribute)
48
+ elsif caster = attribute.options[:caster]
49
+ cast_children_with_caster(casted_value, attribute, caster)
50
+ else
51
+ casted_value
52
+ end
53
+ end
54
+ end
55
+
56
+ def cast_children(value, attribute)
57
+ caster = self.class.new(attribute.children, options)
58
+ cast_children_with_caster(value, attribute, caster)
59
+ end
60
+
61
+ def cast_children_with_caster(value, attribute, caster)
62
+ if attribute.caster == HCast::Casters::ArrayCaster
63
+ value.map do |val|
64
+ caster.cast(val)
65
+ end
66
+ else
67
+ caster.cast(value)
68
+ end
69
+ end
70
+
71
+ def get_keys(hash)
72
+ if options[:input_keys] != options[:output_keys]
73
+ if options[:input_keys] == :symbol
74
+ hash.keys.map(&:to_s)
75
+ else
76
+ hash.keys.map(&:to_sym)
77
+ end
78
+ else
79
+ hash.keys
80
+ end
81
+ end
82
+
83
+ def get_value(hash, key)
84
+ if options[:input_keys] != options[:output_keys]
85
+ if options[:input_keys] == :symbol
86
+ hash[key.to_sym]
87
+ else
88
+ hash[key.to_s]
89
+ end
90
+ else
91
+ hash[key]
92
+ end
93
+ end
94
+
95
+ def check_unexpected_attributes_not_given!(input_hash_keys, casted_hash_keys)
96
+ unexpected_keys = input_hash_keys - casted_hash_keys
97
+ unless unexpected_keys.empty?
98
+ raise HCast::Errors::UnexpectedAttributeError.new("is not valid attribute name", unexpected_keys.first)
99
+ end
100
+ end
101
+
102
+ end
@@ -0,0 +1,63 @@
1
+ # Parses caster rules
2
+ # and returns list of HCast::Metadata::Attribute instances
3
+ # which contains casting rules
4
+ class HCast::AttributesParser
5
+
6
+ # Performs casting
7
+ # @param block [Proc] block with casting rules
8
+ # @return Array(HCast::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 = HCast.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 = HCast::Metadata::Attribute.new(attr_name, caster, options)
37
+ if block_given?
38
+ attribute.children = HCast::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 HCast::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 HCast::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 HCast::Errors::ArgumentError, "attribute options should be a Hash"
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,142 @@
1
+ # Include this module to create your caster
2
+ #
3
+ # Example caster:
4
+ # class ContactCaster
5
+ # include HCast::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 HCast::Caster
80
+ extend HCast::Concern
81
+
82
+ module ClassMethods
83
+
84
+ # Defines casting rules
85
+ # @example
86
+ # attributes do
87
+ # string :first_name
88
+ # string :last_name
89
+ # integer :age, optional: true
90
+ # end
91
+ def attributes(&block)
92
+ raise ArgumentError, "You should provide block" unless block_given?
93
+
94
+ attributes = HCast::AttributesParser.parse(&block)
95
+ self.class_variable_set(:@@attributes, attributes)
96
+ end
97
+
98
+ # Performs casting
99
+ # @param hash [Hash] hash for casting
100
+ # @param options [Hash] options, input_keys: :string, output_key: :symbol
101
+ def cast(hash, options = {})
102
+ check_attributes_defined!
103
+ check_hash_given!(hash)
104
+ check_options!(options)
105
+ set_default_options(options)
106
+
107
+ attributes_caster = HCast::AttributesCaster.new(class_variable_get(:@@attributes), options)
108
+ attributes_caster.cast(hash)
109
+ end
110
+
111
+ private
112
+
113
+ def check_attributes_defined!
114
+ unless class_variable_defined?(:@@attributes)
115
+ raise HCast::Errors::ArgumentError, "Attributes block should be defined"
116
+ end
117
+ end
118
+
119
+ def check_options!(options)
120
+ unless options.is_a?(Hash)
121
+ raise HCast::Errors::ArgumentError, "Options should be a hash"
122
+ end
123
+ if options[:input_keys] && ![:string, :symbol].include?(options[:input_keys])
124
+ raise HCast::Errors::ArgumentError, "input_keys should be :string or :symbol"
125
+ end
126
+ if options[:output_keys] && ![:string, :symbol].include?(options[:output_keys])
127
+ raise HCast::Errors::ArgumentError, "output_keys should be :string or :symbol"
128
+ end
129
+ end
130
+
131
+ def check_hash_given!(hash)
132
+ unless hash.is_a?(Hash)
133
+ raise HCast::Errors::ArgumentError, "Hash should be given"
134
+ end
135
+ end
136
+
137
+ def set_default_options(options)
138
+ options[:input_keys] ||= HCast.config.input_keys
139
+ options[:output_keys] ||= HCast.config.output_keys
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,32 @@
1
+ class HCast::Casters::ArrayCaster
2
+
3
+ def self.cast(value, attr_name, options = {})
4
+ if value.is_a?(Array)
5
+ if options[:each]
6
+ cast_array_items(value, attr_name, options)
7
+ else
8
+ value
9
+ end
10
+ else
11
+ raise HCast::Errors::CastingError, "should be an array"
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def self.cast_array_items(array, attr_name, options)
18
+ caster_name = options[:each]
19
+ caster = HCast.casters[caster_name]
20
+ check_caster_exists!(caster, caster_name)
21
+ array.map do |item|
22
+ caster.cast(item, "#{attr_name} item", options)
23
+ end
24
+ end
25
+
26
+ def self.check_caster_exists!(caster, caster_name)
27
+ unless caster
28
+ raise HCast::Errors::CasterNotFoundError, "caster with name #{caster_name} is not found"
29
+ end
30
+ end
31
+
32
+ end
@@ -0,0 +1,15 @@
1
+ class HCast::Casters::BooleanCaster
2
+
3
+ def self.cast(value, attr_name, options = {})
4
+ if [TrueClass, FalseClass].include?(value.class)
5
+ value
6
+ elsif ['1', 'true', 'on', 1].include?(value)
7
+ true
8
+ elsif ['0', 'false', 'off', 0].include?(value)
9
+ false
10
+ else
11
+ raise HCast::Errors::CastingError, "should be a boolean"
12
+ end
13
+ end
14
+
15
+ end
@@ -0,0 +1,19 @@
1
+ require 'date'
2
+
3
+ class HCast::Casters::DateCaster
4
+
5
+ def self.cast(value, attr_name, options = {})
6
+ if value.is_a?(Date)
7
+ value
8
+ elsif value.is_a?(String)
9
+ begin
10
+ Date.parse(value)
11
+ rescue ArgumentError => e
12
+ raise HCast::Errors::CastingError, "is invalid date"
13
+ end
14
+ else
15
+ raise HCast::Errors::CastingError, "should be a date"
16
+ end
17
+ end
18
+
19
+ end
@@ -0,0 +1,19 @@
1
+ class HCast::Casters::DateTimeCaster
2
+
3
+ def self.cast(value, attr_name, options = {})
4
+ if value.is_a?(DateTime)
5
+ value
6
+ elsif value.is_a?(Time)
7
+ value.to_datetime
8
+ elsif value.is_a?(String)
9
+ begin
10
+ DateTime.parse(value)
11
+ rescue ArgumentError => e
12
+ raise HCast::Errors::CastingError, "is invalid datetime"
13
+ end
14
+ else
15
+ raise HCast::Errors::CastingError, "should be a datetime"
16
+ end
17
+ end
18
+
19
+ end
@@ -0,0 +1,17 @@
1
+ class HCast::Casters::FloatCaster
2
+
3
+ def self.cast(value, attr_name, options = {})
4
+ if value.is_a?(Float)
5
+ value
6
+ elsif value.is_a?(String)
7
+ begin
8
+ Float(value)
9
+ rescue ArgumentError => e
10
+ raise HCast::Errors::CastingError, "is invalid float"
11
+ end
12
+ else
13
+ raise HCast::Errors::CastingError, "should be a float"
14
+ end
15
+ end
16
+
17
+ end
@@ -0,0 +1,11 @@
1
+ class HCast::Casters::HashCaster
2
+
3
+ def self.cast(value, attr_name, options = {})
4
+ if value.is_a?(Hash)
5
+ value
6
+ else
7
+ raise HCast::Errors::CastingError, "should be a hash"
8
+ end
9
+ end
10
+
11
+ end
@@ -0,0 +1,17 @@
1
+ class HCast::Casters::IntegerCaster
2
+
3
+ def self.cast(value, attr_name, options = {})
4
+ if value.is_a?(Integer)
5
+ value
6
+ elsif value.is_a?(String)
7
+ begin
8
+ Integer(value)
9
+ rescue ArgumentError => e
10
+ raise HCast::Errors::CastingError, "is invalid integer"
11
+ end
12
+ else
13
+ raise HCast::Errors::CastingError, "should be a integer"
14
+ end
15
+ end
16
+
17
+ end