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 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