scrap_cbf_record 0.1.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: 1689f847640b2f8bcfd5e9362b785d4b703bddd402097082f0c8f6bace631afe
4
+ data.tar.gz: 0fca0b02a69c838721a5f60fc1228e059bb35a2dd95f383b4d5cec94ca13b0d3
5
+ SHA512:
6
+ metadata.gz: 4600056001b3c311ee5a9ea08ec1a3e35b6a1aa3502002afa8fc54c693c00bbf7094126f8a835418ddbb6164ab4b50bcf82680f255d8adf850da84976006766d
7
+ data.tar.gz: 2857154aa9b4acff0221fa0ad6447d3d77eb685bbf61584ae795f15c85b55b1a24f5b0c677d49408163e928ae11bf8af8c3017eeae33ba41a1649c627cb21e20
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'scrap_cbf_record/version'
4
+ require_relative 'scrap_cbf_record/logger'
5
+ require_relative 'scrap_cbf_record/errors'
6
+ require_relative 'scrap_cbf_record/models/base'
7
+ require_relative 'scrap_cbf_record/models/championship'
8
+ require_relative 'scrap_cbf_record/models/match'
9
+ require_relative 'scrap_cbf_record/models/ranking'
10
+ require_relative 'scrap_cbf_record/models/round'
11
+ require_relative 'scrap_cbf_record/models/team'
12
+ require_relative 'scrap_cbf_record/models/concerns/active_record_relation'
13
+ require_relative 'scrap_cbf_record/config'
14
+ require_relative 'scrap_cbf_record/active_record'
15
+
16
+ # This module saves on database the output from the gem ScrapCbf
17
+ #
18
+ # It has two modules to accomplish that:
19
+ # - configs: holds the settings for how to save the data
20
+ # - records: responsible for saving the data on database.
21
+ #
22
+ # There configs are:
23
+ # - championship
24
+ # - match
25
+ # - ranking
26
+ # - round
27
+ # - team
28
+ #
29
+ # The records are:
30
+ # - matches: saves the matches for a specific championship
31
+ # - rankings: saves the rankings for a specific championship
32
+ # - rounds: saves the rounds for a specific championship
33
+ # - teams: saves the teams that participated on the specific champ.
34
+ class ScrapCbfRecord
35
+ class << self
36
+ # Returns the global configurations for these module
37
+ #
38
+ # @return [ScrapCbfRecord::Config]
39
+ def config
40
+ @config ||= settings
41
+ end
42
+
43
+ # Sets the global configurations
44
+ # We can set the configurations in the following way:
45
+ #
46
+ # ScrapCbfRecord.settings do |config|
47
+ # config.championship = {
48
+ # class_name: 'Championship'
49
+ # rename_attrs: {},
50
+ # exclude_attrs_on_create: %i[],
51
+ # exclude_attrs_on_update: %i[],
52
+ # associations: %i[]
53
+ # }
54
+ #
55
+ # config.match = { ... }
56
+ # config.ranking = { ... }
57
+ # config.round = { ... }
58
+ # config.team = { ... }
59
+ # end
60
+ # If a config or a config's attribute was not set,
61
+ # default setting will be used
62
+ #
63
+ # @return [ScrapCbfRecord::Config]
64
+ def settings
65
+ configuration = Config.instance
66
+
67
+ yield configuration if block_given?
68
+
69
+ @config = configuration
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'active_support/core_ext/hash/indifferent_access'
5
+ require 'active_support/core_ext/hash/except'
6
+
7
+ require_relative 'active_records/base'
8
+ require_relative 'active_records/matches'
9
+ require_relative 'active_records/rankings'
10
+ require_relative 'active_records/rounds'
11
+ require_relative 'active_records/teams'
12
+
13
+ class ScrapCbfRecord
14
+ # This module uses Active Record module to save data on database.
15
+ class ActiveRecord
16
+ class << self
17
+ def save(records)
18
+ new(records).save
19
+
20
+ true
21
+ end
22
+ end
23
+
24
+ # The argument records is a hash (json or not) with the following look :
25
+ # - hash[:championship] the championship for a specific year and divison
26
+ # - hash[:matches] the matches for the specific championship
27
+ # - hash[:rankings] the rankings for the specific championship
28
+ # - hash[:rounds] the rounds for the specific championship
29
+ # - hash[:teams] the teams that participated on the specific championship
30
+ #
31
+ # @param [records] hash or json returned from ScrapCbf gem
32
+ # @return [nil]
33
+ def initialize(records)
34
+ records = parse_json!(records) if records.is_a?(String)
35
+
36
+ raise ::ArgumentError, invalid_type_message unless records.is_a?(Hash)
37
+
38
+ @records = records.with_indifferent_access
39
+
40
+ validate_record_key_presence!(@records)
41
+
42
+ @championship = @records[:championship]
43
+ @matches = @records[:matches]
44
+ @rankings = @records[:rankings]
45
+ @rounds = @records[:rounds]
46
+ @teams = @records[:teams]
47
+ end
48
+
49
+ # Save records to the database.
50
+ # Note: Because of database relationships and dependencies between records
51
+ # there maybe exist a saving order.
52
+ # - Teams must be save before Rankings and Match.
53
+ # - Rounds must be save before Matches
54
+ #
55
+ # @raise [ActiveRecordValidationError] in case of failing while saving
56
+ #
57
+ # @return [Boolean] true in case of success
58
+ def save
59
+ save_teams(@teams)
60
+ save_rankings(@rankings, @championship)
61
+ save_rounds(@rounds, @championship)
62
+ save_matches(@matches, @championship)
63
+
64
+ true
65
+ end
66
+
67
+ private
68
+
69
+ def save_teams(teams)
70
+ Teams.new(teams).create_unless_found
71
+ end
72
+
73
+ def save_rankings(rankings, championship)
74
+ Rankings.new(rankings).create_or_update(championship)
75
+ end
76
+
77
+ def save_rounds(rounds, championship)
78
+ Rounds.new(rounds).create_unless_found(championship)
79
+ end
80
+
81
+ def save_matches(matches, championship)
82
+ Matches.new(matches).create_or_update(championship)
83
+ end
84
+
85
+ def invalid_type_message
86
+ 'must be a Hash or Json of a Hash'
87
+ end
88
+
89
+ def parse_json!(records)
90
+ JSON.parse(records)
91
+ rescue JSON::ParserError => e
92
+ raise JsonDecodeError, e
93
+ end
94
+
95
+ def validate_record_key_presence!(records)
96
+ raise MissingKeyError, 'championship' unless records.key?(:championship)
97
+ raise MissingKeyError, 'matches' unless records.key?(:matches)
98
+ raise MissingKeyError, 'rankings' unless records.key?(:rankings)
99
+ raise MissingKeyError, 'rounds' unless records.key?(:rounds)
100
+ raise MissingKeyError, 'teams' unless records.key?(:teams)
101
+
102
+ true
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ScrapCbfRecord
4
+ class ActiveRecord
5
+ # Superclass for the classes lib/active_records/<record>.rb
6
+ class Base
7
+ def initialize(config)
8
+ # config is associated with the current record class
9
+ #
10
+ @config = config
11
+
12
+ # This reference the user class used to save on database
13
+ #
14
+ @model = config.klass
15
+
16
+ # current configs set by users
17
+ #
18
+ @associations = @config.associations
19
+ @exclude_attrs_on_create = @config.exclude_attrs_on_create
20
+ @exclude_attrs_on_update = @config.exclude_attrs_on_update
21
+ @rename_attrs = @config.rename_attrs
22
+
23
+ # current configs required by the system
24
+ #
25
+ @must_exclude_attrs = @config.must_exclude_attrs
26
+ end
27
+
28
+ # Normalize, for create and update, the new record with:
29
+ # Setting Associations
30
+ # Rename attributes
31
+ # Exclusion of attributes
32
+ #
33
+ # @param [Object, Nil] the record if exist
34
+ # @param [Hash] contaning the new record
35
+ # @param [Hash] contaning the existent record's associations
36
+ # @return [Object] normalized
37
+ def normalize_before_save(record, hash, associations = {})
38
+ if record
39
+ hash = normalize_before_update(hash, associations)
40
+
41
+ record.assign_attributes(hash)
42
+
43
+ record
44
+ else
45
+ hash = normalize_before_create(hash, associations)
46
+
47
+ @model.new(hash)
48
+ end
49
+ end
50
+
51
+ # Normalize, on create, the new record with:
52
+ # Setting Associations
53
+ # Rename attributes
54
+ # Exclusion of attributes
55
+ #
56
+ # @param [Hash] contaning the new record
57
+ # @param [Hash] contaning the existent record's associations
58
+ # @return [Hash] normalized
59
+ def normalize_before_create(hash, assocs = {})
60
+ hash = include_associations(
61
+ hash,
62
+ @associations,
63
+ assocs
64
+ )
65
+
66
+ hash = exclude_attrs(
67
+ hash,
68
+ @exclude_attrs_on_create,
69
+ @must_exclude_attrs,
70
+ @associations.keys
71
+ )
72
+
73
+ hash = rename_attrs(hash, @rename_attrs)
74
+
75
+ hash
76
+ end
77
+
78
+ # Normalize, on update, the new record with:
79
+ # Setting Associations
80
+ # Rename attributes
81
+ # Exclusion of attributes
82
+ #
83
+ # @param [Hash] contaning the new record
84
+ # @param [Hash] contaning the existent record's associations
85
+ # @return [Hash] normalized
86
+ def normalize_before_update(hash, assocs = {})
87
+ hash = include_associations(
88
+ hash,
89
+ @associations,
90
+ assocs
91
+ )
92
+
93
+ hash = exclude_attrs(
94
+ hash,
95
+ @exclude_attrs_on_update,
96
+ @must_exclude_attrs,
97
+ @associations.keys
98
+ )
99
+
100
+ hash = rename_attrs(hash, @rename_attrs)
101
+
102
+ hash
103
+ end
104
+
105
+ # Save record instance or log the errors found
106
+ #
107
+ # @raise [ActiveRecordValidationError]
108
+ # @param record [ActiveRecord] instance to be saved
109
+ # @return [ActiveRecord] the instance saved
110
+ def save_or_log_error(record)
111
+ if record.valid?
112
+ record.save
113
+ else
114
+ log_record_errors(record)
115
+ # It raises custom error to display message warning about the logs.
116
+ raise ActiveRecordValidationError
117
+ end
118
+ record
119
+ end
120
+
121
+ protected
122
+
123
+ # Includes associations <attribute_id> and its value to the record hash
124
+ # only include if the associations was set by the user.
125
+ #
126
+ # @param hash [Hash] record hash to be modified
127
+ # @param associations [Hash] hash contaning associations details
128
+ # @param assocs [Hash] hash contaning the associations
129
+ # values returned by find_by
130
+ # @return [Hash] record hash with associations included
131
+ def include_associations(hash, associations, assocs)
132
+ associations.each do |name, attrs|
133
+ instance = assocs[name.to_sym]
134
+
135
+ foreign_key = attrs[:foreign_key]
136
+
137
+ # for cases where instance is:
138
+ # - association is empty (nil)
139
+ # - association is present
140
+ #
141
+ # update hash with the foreign_key
142
+ hash[foreign_key.to_sym] = (instance.id if instance.present?)
143
+ end
144
+
145
+ hash
146
+ end
147
+
148
+ # Exclude some keys from the record hash
149
+ #
150
+ # @param hash [Hash] record hash to be modified
151
+ # @param attrs [Array] has the keys to be exclude.
152
+ # These attrs are the union of exclude_attrs_on_create/update.
153
+ # @param must_exclude [Array] has the keys to be excluded.
154
+ # The keys are set by the lib, and is used to remove unwanted attrs.
155
+ # @param associations [Array] has the names of the associations.
156
+ # include_associations method does't remove the associations added.
157
+ # e.g if championship association exist, then:
158
+ # add championship_id to hash.
159
+ # But, it doesn't remove the championship key from the hash.
160
+ # It's remove here.
161
+ # @return [Hash] record hash modified
162
+ def exclude_attrs(hash, attrs, must_exclude, associations)
163
+ exclude = attrs + associations + must_exclude
164
+ hash.except(*exclude)
165
+ end
166
+
167
+ # Rename keys from the record hash
168
+ #
169
+ # @param hash [Hash] record hash to be modified
170
+ # @param renames [Hash] has the keys to be renamed by the key's value.
171
+ # @return [Hash] record hash modified
172
+ def rename_attrs(hash, renames)
173
+ # rename attrs
174
+ renames.each do |key, val|
175
+ hash[val.to_sym] = hash.delete(key) if hash.key?(key)
176
+ end
177
+
178
+ hash
179
+ end
180
+
181
+ def raise_unless_respond_to_each(records, records_type)
182
+ return if records.respond_to?(:each)
183
+
184
+ raise ::ArgumentError, "#{records_type} must respond to method :each"
185
+ end
186
+
187
+ def log_record_errors(record)
188
+ TagLogger.with_context([Time.current], 'Errors found while saving')
189
+ record.errors.each do |attribute, message|
190
+ TagLogger.with_context(:info, record.inspect.to_s)
191
+ TagLogger.with_context(:error, "#{attribute}: #{message}")
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ScrapCbfRecord
4
+ class ActiveRecord
5
+ # Class responsible for saving matches to database
6
+ class Matches < Base
7
+ #
8
+ # @param [matches] hash contaning the matches
9
+ # @return [nil]
10
+ def initialize(matches)
11
+ raise_unless_respond_to_each(matches, :matches)
12
+
13
+ configurations = ScrapCbfRecord.config
14
+
15
+ super(configurations.match)
16
+
17
+ @matches = matches
18
+ end
19
+
20
+ # Creates/Updates the matches found on the instance variable matches
21
+ # Update if match already exist, otherwise create it
22
+ #
23
+ # @param [championship_hash] the championship associated with the matches
24
+ # @raise [ActiveRecordValidationError] if fail on saving
25
+ # @return [Boolean] true if not exception is raise
26
+ def create_or_update(championship_hash)
27
+ championship, serie = find_championship_by(championship_hash)
28
+
29
+ ::ActiveRecord::Base.transaction do
30
+ @matches.each do |hash|
31
+ attrs = [hash, championship, serie]
32
+ match = find_match_by(*attrs)
33
+ round = find_round_by(*attrs)
34
+ team = Match.team.find_by(name: hash[:team])
35
+ opponent = Match.opponent.find_by(name: hash[:opponent])
36
+
37
+ associations = {
38
+ championship: championship,
39
+ round: round,
40
+ team: team,
41
+ opponent: opponent
42
+ }
43
+
44
+ match = normalize_before_save(match, hash, associations)
45
+
46
+ save_or_log_error(match)
47
+ end
48
+ end
49
+ true
50
+ end
51
+
52
+ private
53
+
54
+ def find_championship_by(championship_hash)
55
+ championship =
56
+ Match.championship.find_by!(year: championship_hash[:year])
57
+
58
+ serie = championship_hash[:serie]
59
+
60
+ [championship, serie]
61
+ end
62
+
63
+ def find_match_by(hash, championship, serie)
64
+ Match.find_by(
65
+ id_match: hash[:id_match],
66
+ championship: championship,
67
+ serie: serie
68
+ )
69
+ end
70
+
71
+ def find_round_by(hash, championship, serie)
72
+ Match.round.find_by(
73
+ number: hash[:round],
74
+ championship: championship,
75
+ serie: serie
76
+ )
77
+ end
78
+ end
79
+ end
80
+ end