scrap_cbf_record 0.1.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: 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