Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rebuild primary workflow classes #569

Merged
merged 20 commits into from
Aug 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .reek.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,9 @@ detectors:
accept:
- "e"
- "_"
UnusedParameters:
exclude:
- "BaseNormalizer#normalize"
- "BaseProcessor#process"
UtilityFunction:
public_methods_only: true
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@ source "https://rubygems.org"

ruby "3.3.4"

gem "aasm", "~> 5.5"
gem "amazing_print"
gem "bootsnap", require: false
gem "jsbundling-rails"
gem "memo_wise", "~> 1.9"
gem "pg", "~> 1.5"
gem "propshaft"
gem "pry", "~> 0.14"
gem "pry-byebug"
gem "pry-rails", "~> 0.3.9"
gem "puma", "~> 6.4"
gem "rails", "~> 7.1.3", ">= 7.1.3.4"
gem "rexml", ">= 3.3.4" # No direct dependency; added to mitigate a CVE
gem "stimulus-rails"
gem "turbo-rails"
gem "tzinfo-data"
Expand Down
25 changes: 14 additions & 11 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ GEM
base64 (0.2.0)
bigdecimal (3.1.8)
bindex (0.8.1)
bootsnap (1.18.3)
bootsnap (1.18.4)
msgpack (~> 1.2)
brakeman (6.1.2)
racc
Expand All @@ -97,7 +97,7 @@ GEM
thor (~> 1.0)
byebug (11.1.3)
coderay (1.1.3)
concurrent-ruby (1.3.3)
concurrent-ruby (1.3.4)
connection_pool (2.4.1)
crack (1.0.0)
bigdecimal
Expand Down Expand Up @@ -145,14 +145,14 @@ GEM
railties (>= 5.0.0)
globalid (1.2.1)
activesupport (>= 6.1)
hashdiff (1.1.0)
hashdiff (1.1.1)
i18n (1.14.5)
concurrent-ruby (~> 1.0)
io-console (0.7.2)
irb (1.14.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jsbundling-rails (1.3.0)
jsbundling-rails (1.3.1)
railties (>= 6.0.0)
json (2.7.2)
language_server-protocol (3.17.0.3)
Expand All @@ -169,6 +169,7 @@ GEM
marginalia (1.11.1)
actionpack (>= 5.2)
activerecord (>= 5.2)
memo_wise (1.9.0)
method_source (1.1.0)
mini_mime (1.1.5)
minitest (5.24.1)
Expand Down Expand Up @@ -196,8 +197,8 @@ GEM
racc (~> 1.4)
nokogiri (1.16.7-x86_64-linux)
racc (~> 1.4)
parallel (1.25.1)
parser (3.3.4.0)
parallel (1.26.1)
parser (3.3.4.2)
ast (~> 2.4.1)
racc
pg (1.5.7)
Expand All @@ -219,7 +220,7 @@ GEM
public_suffix (6.0.1)
puma (6.4.2)
nio4r (~> 2.0)
racc (1.8.0)
racc (1.8.1)
rack (3.1.7)
rack-session (2.0.0)
rack (>= 3.0.0)
Expand Down Expand Up @@ -269,7 +270,7 @@ GEM
regexp_parser (2.9.2)
reline (0.5.9)
io-console (~> 0.5)
rexml (3.3.2)
rexml (3.3.4)
strscan
rspec-core (3.13.0)
rspec-support (~> 3.13.0)
Expand Down Expand Up @@ -299,7 +300,7 @@ GEM
rubocop-ast (>= 1.31.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.31.3)
rubocop-ast (1.32.0)
parser (>= 3.3.1.0)
rubocop-factory_bot (2.26.1)
rubocop (~> 1.61)
Expand All @@ -311,7 +312,7 @@ GEM
rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rspec (3.0.3)
rubocop-rspec (3.0.4)
rubocop (~> 1.61)
rubocop-rspec_rails (2.30.0)
rubocop (~> 1.61)
Expand Down Expand Up @@ -364,7 +365,7 @@ GEM
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
yaml-lint (0.1.2)
zeitwerk (2.6.16)
zeitwerk (2.6.17)

PLATFORMS
aarch64-linux
Expand All @@ -385,6 +386,7 @@ DEPENDENCIES
factory_bot_rails (~> 6.4)
jsbundling-rails
marginalia (~> 1.11)
memo_wise (~> 1.9)
pg (~> 1.5)
propshaft
pry (~> 0.14)
Expand All @@ -393,6 +395,7 @@ DEPENDENCIES
puma (~> 6.4)
rails (~> 7.1.3, >= 7.1.3.4)
reek (~> 6.3)
rexml (>= 3.3.4)
rspec-rails (~> 6.1)
rubocop
rubocop-factory_bot
Expand Down
2 changes: 2 additions & 0 deletions app/errors/abstract_method_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class AbstractMethodError < StandardError
end
2 changes: 2 additions & 0 deletions app/errors/feed_configuration_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class FeedConfigurationError < StandardError
end
6 changes: 6 additions & 0 deletions app/loaders/base_loader.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class BaseLoader < FeedService
# @return [FeedContent]
def load
raise AbstractMethodError
end
end
3 changes: 3 additions & 0 deletions app/models/error_report.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class ErrorReport < ApplicationRecord
belongs_to :target, polymorphic: true, optional: true
end
48 changes: 48 additions & 0 deletions app/models/feed.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
class Feed < ApplicationRecord
def readable_id
[self.class.name.underscore, id, name].compact_blank.join("-")
end

def ensure_supported
return if loader_class && processor_class && normalizer_class
raise FeedConfigurationError
end

def service_classes
{
loader_class: loader_class,
processor_class: processor_class,
normalizer_class: normalizer_class
}
end

def loader_class
ClassResolver.new(loader, suffix: "loader").resolve
rescue NameError
nil
end

def loader_instance
loader_class.new(self)
end

def processor_class
ClassResolver.new(processor, suffix: "processor").resolve
rescue NameError
nil
end

def processor_instance
processor_class.new(self)
end

def normalizer_class
ClassResolver.new(normalizer, suffix: "normalizer").resolve
rescue NameError
nil
end

def normalizer_instance
normalizer_class.new(self)
end
end
14 changes: 14 additions & 0 deletions app/models/feed_content.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Data object for raw feed content representation. All loaders return
# a FeedContent instance.
#
# @see BaseLoader
#
class FeedContent
attr_reader :raw_content, :imported_at, :import_duration

def initialize(raw_content:, imported_at:, import_duration:)
@raw_content = raw_content
@imported_at = imported_at
@import_duration = import_duration
end
end
13 changes: 13 additions & 0 deletions app/models/feed_entity.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Data object representating a raw feed entity (like a blog post or RSS item).
#
class FeedEntity
attr_reader :feed, :uid, :content

# @param uid: [String] unique entity identifier, like RSS item id
# or permalink URL
# @content [Object] arbitrary content representation
def initialize(uid:, content:)
@uid = uid
@content = content
end
end
7 changes: 7 additions & 0 deletions app/normalizers/base_normalizer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class BaseNormalizer < FeedService
# @param [FeedEntity]
# @return [Post]
def normalize(feed_entity:)
raise AbstractMethodError
end
end
7 changes: 7 additions & 0 deletions app/processors/base_processor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class BaseProcessor < FeedService
# @param [FeedContent]
# @return [Array<FeedEntity>]
def process(feed_content:)
raise AbstractMethodError
end
end
69 changes: 69 additions & 0 deletions app/services/application_logger.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# ApplicationLogger is a wrapper for Ruby loggers that adds color formatting to
# log messages. Color formatting can be enabled or disabled, and it respects the
# NO_COLOR environment variable by default.
#
# Usage:
# logger = ApplicationLogger.new
# logger.info "This is an info message"
# logger.info { "This line will be evaluated only if the logger" }
#
class ApplicationLogger
CLEAR = "\e[0m"
BOLD = "\e[1m"

BLACK = "\e[30m"
RED = "\e[31m"
GREEN = "\e[32m"
YELLOW = "\e[33m"
BLUE = "\e[34m"
MAGENTA = "\e[35m"
CYAN = "\e[36m"
WHITE = "\e[37m"

attr_reader :logger

def initialize(logger: Rails.logger, colorize: ENV["NO_COLOR"].blank?)
@logger = logger
@colorize = colorize
end

def debug(message = nil, &)
log_formatted_message(:debug, message, WHITE, &)
end

def info(message = nil, &)
log_formatted_message(:info, message, BLUE, &)
end

def warn(message = nil, &)
log_formatted_message(:warn, message, YELLOW, &)
end

def error(message = nil, &)
log_formatted_message(:error, message, RED, &)
end

def success(message = nil, &)
log_formatted_message(:info, message, GREEN, &)
end

def log_formatted_message(level, message, color, &block)
if block
logger.send(level, &-> { format_message(block.call, color) })
else
logger.send(level, format_message(message, color))
end
end

def format_message(message, color)
if colorize?
"#{color}#{message}#{CLEAR}"
else
message
end
end

def colorize?
@colorize
end
end
19 changes: 19 additions & 0 deletions app/services/class_resolver.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Resolver a class from the class name and optional suffix.
#
class ClassResolver
attr_reader :class_name, :suffix

# @param [String] a string representing the target class name with words
# separated by underscores
# @param suffix: [String] optional suffix for the class name
def initialize(class_name, suffix: nil)
@class_name = class_name
@suffix = suffix
end

# @return [Class]
# @raise [NameError] if the target class is missing
def resolve
[class_name, suffix].join("_").classify.constantize
end
end
41 changes: 41 additions & 0 deletions app/services/error_reporter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Hidrate new instance of error report with
class ErrorReporter
# @param error: [Exception]
# @params category: [String]
# @param target: [ActiveRecord::Base] target record (optional)
# @param context: [Hash] JSON-friendly arbitrary hash object (optional)
# @param occured_at: [DateTime] error timestamp (optional)
def report(error:, category:, **options)
ErrorReport.create!(
category: category,
target: options[:target],
context: options[:context] || {},
occured_at: options[:occured_at] || Time.current,
**error_details(error)
)
end

private

def error_details(error)
{
error_class: error.class.name,
message: error.message,
**backtrace_dtails(error.backtrace || [])
}
end

def backtrace_dtails(backtrace)
match = backtrace&.first&.match(/^(.+):(\d+):in/)

if match
{
backtrace: backtrace,
file_name: match[1],
line_number: match[2].to_i
}
else
{backtrace: backtrace}
end
end
end
Loading
Loading