Finish December dispatch
This commit is contained in:
792
static/archive/allaboutcoding-ghinda-com-jnjy0d.txt
Normal file
792
static/archive/allaboutcoding-ghinda-com-jnjy0d.txt
Normal file
@@ -0,0 +1,792 @@
|
||||
|
||||
(BUTTON)
|
||||
|
||||
Lucian Ghinda
|
||||
|
||||
All about coding
|
||||
|
||||
(BUTTON) (BUTTON)
|
||||
Follow
|
||||
|
||||
[1]All about coding
|
||||
|
||||
Follow
|
||||
Ruby open source: feedbin Ruby open source: feedbin
|
||||
|
||||
Ruby open source: feedbin
|
||||
|
||||
[2]Lucian Ghinda's photo Lucian Ghinda's photo
|
||||
[3]Lucian Ghinda
|
||||
·[4]Nov 17, 2023·
|
||||
|
||||
11 min read
|
||||
|
||||
Table of contents
|
||||
|
||||
* [5]The product
|
||||
* [6]Open source
|
||||
+ [7]License
|
||||
* [8]Technical review
|
||||
+ [9]Ruby and Rails version
|
||||
+ [10]Architecture
|
||||
+ [11]Stats
|
||||
+ [12]Style Guide
|
||||
+ [13]Storage, Persistence and in-memory storage
|
||||
+ [14]Gems used
|
||||
+ [15]Code & Design Patterns
|
||||
o [16]Code Organisation
|
||||
o [17]Routes
|
||||
o [18]Controllers
|
||||
o [19]Models
|
||||
o [20]Jobs
|
||||
o [21]Presenters
|
||||
o [22]ApplicationComponents
|
||||
o [23]ComponentsPreview
|
||||
* [24]Testing
|
||||
+ [25]Custom assertions
|
||||
* [26]Conclusion
|
||||
|
||||
The product
|
||||
|
||||
[27]https://feedbin.com
|
||||
|
||||
"Feedbin is the best way to enjoy content on the Web. By combining
|
||||
RSS, and newsletters, you can get all the good parts of the Web in
|
||||
one convenient location"
|
||||
|
||||
Open source
|
||||
|
||||
The open-source repository can be found at
|
||||
[28]https://github.com/feedbin/feedbin
|
||||
|
||||
License
|
||||
|
||||
The [29]license they use is MIT:
|
||||
|
||||
Technical review
|
||||
|
||||
Ruby and Rails version
|
||||
|
||||
They are currently using:
|
||||
* Ruby version 3.2.2
|
||||
* They used a fork of Rails at [30]https://github.com/feedbin/rails
|
||||
forked from [31]https://github.com/Shopify/rails. They are using a
|
||||
branch called [32]7-1-stable-invalid-cache-entries - It seems to be
|
||||
Rails 7.1 and about 1 month behind the Shopify/rails which is
|
||||
usually pretty up to date with main Rails
|
||||
|
||||
Architecture
|
||||
|
||||
Code Architecture:
|
||||
* They are using the standard Rails organisation of MVC.
|
||||
|
||||
Database:
|
||||
* The DB is PostgreSQL
|
||||
|
||||
Jobs queue:
|
||||
* Sidekiq
|
||||
|
||||
On the front-end side:
|
||||
* They use .html.erb
|
||||
* They are using Phlex for [33]components
|
||||
* They are using [34]Jquery for the JS library
|
||||
* They have some custom JS code written in [35]CoffeeScript
|
||||
* They are using Hotwire via [36]importmaps
|
||||
* They are using [37]Tailwind
|
||||
|
||||
Stats
|
||||
|
||||
Running /bin/rails stats will output the following:
|
||||
|
||||
Running VSCodeCounter will give the following stats:
|
||||
|
||||
Style Guide
|
||||
|
||||
For Ruby:
|
||||
* They are using [38]standardrb as the Style Guide with no
|
||||
customisations.
|
||||
|
||||
Storage, Persistence and in-memory storage
|
||||
|
||||
The DB is PostgreSQL.
|
||||
|
||||
They are not using the schema.rb but the [39]structure.sql format for
|
||||
DB schema dump is configured via application.rb:
|
||||
module Feedbin
|
||||
class Application < Rails::Application
|
||||
# other configs
|
||||
config.active_record.schema_format = :sql
|
||||
# other configs
|
||||
end
|
||||
end
|
||||
|
||||
Enabled PSQL extensions:
|
||||
* hstore - "data type for storing sets of (key, value) pairs"
|
||||
* pg_stat_statements - "track planning and execution statistics of
|
||||
all SQL statements executed"
|
||||
* uuid-ossp - "generate universally unique identifiers (UUIDs)"
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS hstore WITH SCHEMA public;
|
||||
COMMENT ON EXTENSION hstore IS 'data type for storing sets of (key, value) pairs
|
||||
';
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS pg_stat_statements WITH SCHEMA public;
|
||||
COMMENT ON EXTENSION pg_stat_statements IS 'track planning and execution statist
|
||||
ics of all SQL statements executed';
|
||||
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA public;
|
||||
COMMENT ON EXTENSION "uuid-ossp" IS 'generate universally unique identifiers (UU
|
||||
IDs)';
|
||||
|
||||
Redis is configured to be used with Sidekiq.
|
||||
|
||||
This is what the [40]redis initializer looks like:
|
||||
# https://github.com/feedbin/feedbin/blob/main/config/initializers/redis.rb#L1
|
||||
|
||||
defaults = {connect_timeout: 5, timeout: 5}
|
||||
defaults[:url] = ENV["REDIS_URL"] if ENV["REDIS_URL"]
|
||||
|
||||
$redis = {}.tap do |hash|
|
||||
options2 = defaults.dup
|
||||
if ENV["REDIS_URL_PUBLIC_IDS"] || ENV["REDIS_URL_CACHE"]
|
||||
options2[:url] = ENV["REDIS_URL_PUBLIC_IDS"] || ENV["REDIS_URL_CACHE"]
|
||||
end
|
||||
hash[:refresher] = ConnectionPool.new(size: 10) { Redis.new(options2) }
|
||||
end
|
||||
|
||||
Further, there is a [41]RedisLock configured like this:
|
||||
# https://github.com/feedbin/feedbin/blob/main/app/models/redis_lock.rb#L1
|
||||
|
||||
class RedisLock
|
||||
def self.acquire(lock_name, expiration_in_seconds = 55)
|
||||
Sidekiq.redis { _1.set(lock_name, "locked", ex: expiration_in_seconds, nx: t
|
||||
rue) }
|
||||
end
|
||||
end
|
||||
|
||||
Further down this is used in a [42]clock.rb (that defines scheduled
|
||||
tasks to run):
|
||||
# https://github.com/feedbin/feedbin/blob/main/lib/clock.rb#L8
|
||||
|
||||
every(10.seconds, "clockwork.very_frequent") do
|
||||
if RedisLock.acquire("clockwork:send_stats:v3", 8)
|
||||
SendStats.perform_async
|
||||
end
|
||||
|
||||
if RedisLock.acquire("clockwork:cache_entry_views", 8)
|
||||
CacheEntryViews.perform_async(nil, true)
|
||||
end
|
||||
|
||||
if RedisLock.acquire("clockwork:downloader_migration", 8)
|
||||
FeedCrawler::PersistCrawlData.perform_async
|
||||
end
|
||||
end
|
||||
|
||||
every(1.minutes, "clockwork.frequent") do
|
||||
if RedisLock.acquire("clockwork:feed:refresher:scheduler:v2")
|
||||
FeedCrawler::ScheduleAll.perform_async
|
||||
end
|
||||
|
||||
if RedisLock.acquire("clockwork:harvest:embed:data")
|
||||
HarvestEmbeds.perform_async(nil, true)
|
||||
end
|
||||
end
|
||||
|
||||
every(1.day, "clockwork.daily", at: "7:00", tz: "UTC") do
|
||||
if RedisLock.acquire("clockwork:delete_entries:v2")
|
||||
EntryDeleterScheduler.perform_async
|
||||
end
|
||||
|
||||
if RedisLock.acquire("clockwork:trial_expiration:v2")
|
||||
TrialExpiration.perform_async
|
||||
end
|
||||
|
||||
if RedisLock.acquire("clockwork:web_sub_maintenance")
|
||||
WebSub::Maintenance.perform_async
|
||||
end
|
||||
end
|
||||
|
||||
Gems used
|
||||
|
||||
Here are some of the gems used:
|
||||
* [43]sax-machine - "A declarative sax parsing library backed by
|
||||
Nokogiri"
|
||||
* [44]feedjira - "Feedjira is a Ruby library designed to parse feeds"
|
||||
* [45]html-pipeline - "HTML processing filters and utilities. This
|
||||
module is a small framework for defining CSS-based content filters
|
||||
and applying them to user provided content"
|
||||
* [46]apnotic - "A Ruby APNs HTTP/2 gem able to provide instant
|
||||
feedback"
|
||||
* [47]autoprefixer-rails - "Autoprefixer is a tool to parse CSS and
|
||||
add vendor prefixes to CSS rules using values from the Can I Use
|
||||
database. This gem provides Ruby and Ruby on Rails integration with
|
||||
this JavaScript tool"
|
||||
* [48]clockwork - "Clockwork is a cron replacement. It runs as a
|
||||
lightweight, long-running Ruby process which sits alongside your
|
||||
web processes (Mongrel/Thin) and your worker processes
|
||||
(DJ/Resque/Minion/Stalker) to schedule recurring work at particular
|
||||
times or dates"
|
||||
* [49]down - "Streaming downloads using net/http, http.rb, HTTPX or
|
||||
wget"
|
||||
* [50]phlex-rails - "Phlex is a framework that lets you compose web
|
||||
views in pure Ruby"
|
||||
* [51]premailer-rails - "This gem is a drop in solution for styling
|
||||
HTML emails with CSS without having to do the hard work yourself"
|
||||
* [52]raindrops - "raindrops is a real-time stats toolkit to show
|
||||
statistics for Rack HTTP servers. It is designed for preforking
|
||||
servers such as unicorn, but should support any Rack HTTP server on
|
||||
platforms supporting POSIX shared memory"
|
||||
* [53]strong_migrations - "Catch unsafe migrations in development"
|
||||
* [54]web-push - "This gem makes it possible to send push messages to
|
||||
web browsers from Ruby backends using the Web Push Protocol"
|
||||
* [55]stripe-ruby-mock - "A drop-in library to test stripe without
|
||||
hitting their servers"
|
||||
* [56]rails-controller-testing - "Brings back assigns and
|
||||
assert_template to your Rails tests"
|
||||
|
||||
There are many other gems used, I only selected few here. Browse the
|
||||
[57]Gemfile to discover more.
|
||||
|
||||
What could be mentioned is that they use their fork for some of the
|
||||
gems included in the file:
|
||||
# https://github.com/feedbin/feedbin/blob/main/Gemfile
|
||||
|
||||
# other gems
|
||||
|
||||
gem "rails", github: "feedbin/rails", branch: "7-1-stable-invalid-cache-entries"
|
||||
|
||||
# some other gems
|
||||
|
||||
gem "http", github: "feedbin/http", branch: "feedb
|
||||
in"
|
||||
gem "carrierwave", github: "feedbin/carrierwave", branch: "feedb
|
||||
in"
|
||||
gem "sax-machine", github: "feedbin/sax-machine", branch: "feedb
|
||||
in"
|
||||
gem "feedjira", github: "feedbin/feedjira", branch: "f2"
|
||||
gem "feedkit", github: "feedbin/feedkit", branch: "maste
|
||||
r"
|
||||
gem "html-pipeline", github: "feedbin/html-pipeline", branch: "feedb
|
||||
in"
|
||||
gem "html_diff", github: "feedbin/html_diff", ref: "013e1bb"
|
||||
gem "twitter", github: "feedbin/twitter", branch: "feedb
|
||||
in"
|
||||
|
||||
# other gems
|
||||
|
||||
group :development, :test do
|
||||
gem "stripe-ruby-mock", github: "feedbin/stripe-ruby-mock", branch: "feedbin",
|
||||
require: "stripe_mock"
|
||||
# other gems
|
||||
end
|
||||
|
||||
# other gem groups
|
||||
|
||||
Code & Design Patterns
|
||||
|
||||
Code Organisation
|
||||
|
||||
Under /app there are 3 folders different from the ones that Rails comes
|
||||
with:
|
||||
* presenters
|
||||
* uploaders
|
||||
* validators
|
||||
|
||||
The lib folder includes very few extra objects. Most of them seems to
|
||||
be related to communicating with external services.
|
||||
|
||||
Maybe worth mentioning from lib folder is the
|
||||
[58]ConditionalSassCompressor
|
||||
# https://github.com/feedbin/feedbin/blob/main/lib/conditional_sass_compressor.r
|
||||
b#L1
|
||||
|
||||
class ConditionalSassCompressor
|
||||
def compress(string)
|
||||
return string if string =~ /tailwindcss/
|
||||
options = { syntax: :scss, cache: false, read_cache: false, style: :compress
|
||||
ed}
|
||||
begin
|
||||
Sprockets::Autoload::SassC::Engine.new(string, options).render
|
||||
rescue => e
|
||||
puts "Could not compress '#{string[0..65]}'...: #{e.message}, skipping com
|
||||
pression"
|
||||
string
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
This is used to configure:
|
||||
# https://github.com/feedbin/feedbin/blob/main/config/application.rb#L47
|
||||
config.assets.css_compressor = ConditionalSassCompressor.new
|
||||
|
||||
Routes
|
||||
|
||||
There is a combination of RESTful routes and non-restful routes.
|
||||
|
||||
Here is an example from entries in the [59]routes.rb :
|
||||
# https://github.com/feedbin/feedbin/blob/main/config/routes.rb#L133
|
||||
|
||||
resources :entries, only: [:show, :index, :destroy] do
|
||||
member do
|
||||
post :content
|
||||
post :unread_entries, to: "unread_entries#update"
|
||||
post :starred_entries, to: "starred_entries#update"
|
||||
post :mark_as_read, to: "entries#mark_as_read"
|
||||
post :recently_read, to: "recently_read_entries#create"
|
||||
post :recently_played, to: "recently_played_entries#create"
|
||||
get :push_view
|
||||
get :newsletter
|
||||
end
|
||||
collection do
|
||||
get :starred
|
||||
get :unread
|
||||
get :preload
|
||||
get :search
|
||||
get :recently_read, to: "recently_read_entries#index"
|
||||
get :recently_played, to: "recently_played_entries#index"
|
||||
get :updated, to: "updated_entries#index"
|
||||
post :mark_all_as_read
|
||||
post :mark_direction_as_read
|
||||
end
|
||||
end
|
||||
|
||||
Controllers
|
||||
|
||||
The controllers are mostly what I would call vanilla Rails controllers.
|
||||
|
||||
Three notes about them:
|
||||
* Some of them are responding with JS usually using USJ or JQuery to
|
||||
change elements from the page.
|
||||
* They contain non-Rails standard actions (actions that are not show,
|
||||
index, new, create ...)
|
||||
* There is a namespaced api folder that contains APIs used by mobile
|
||||
apps
|
||||
|
||||
Here is one simple example for DELETE /entries/:id , the controller
|
||||
looks like this:
|
||||
# https://github.com/feedbin/feedbin/blob/main/app/controllers/entries_controlle
|
||||
r.rb#L238
|
||||
def destroy
|
||||
@user = current_user
|
||||
@entry = @user.entries.find(params[:id])
|
||||
if @entry.feed.pages?
|
||||
EntryDeleter.new.delete_entries(@entry.feed_id, @entry.id)
|
||||
end
|
||||
end
|
||||
|
||||
And here is the view [60]destroy.js.erb :
|
||||
$('[data-behavior~=entries_target] [data-entry-id=<%= @entry.id %>]').remove();
|
||||
|
||||
feedbin.Counts.get().removeEntry(<%= @entry.id %>, <%= @entry.feed_id %>, 'unrea
|
||||
d')
|
||||
feedbin.Counts.get().removeEntry(<%= @entry.id %>, <%= @entry.feed_id %>, 'starr
|
||||
ed')
|
||||
feedbin.applyCounts(true)
|
||||
|
||||
feedbin.clearEntry();
|
||||
feedbin.fullScreen(false)
|
||||
|
||||
The main pattern adopted to controllers is to have some logic in them
|
||||
and delegate to jobs some part of the processing.
|
||||
|
||||
The repo contains mostly straight-forward controllers like this one:
|
||||
# https://github.com/feedbin/feedbin/blob/main/app/controllers/pages_internal_co
|
||||
ntroller.rb#L1
|
||||
class PagesInternalController < ApplicationController
|
||||
|
||||
def create
|
||||
@entry = SavePage.new.perform(current_user.id, params[:url], nil)
|
||||
get_feeds_list
|
||||
end
|
||||
end
|
||||
|
||||
But also few controllers that include some logic:
|
||||
# https://github.com/feedbin/feedbin/blob/main/app/controllers/api/podcasts/v1/f
|
||||
eeds_controller.rb#L8
|
||||
|
||||
def show
|
||||
url = hex_decode(params[:id])
|
||||
@feed = Feed.find_by_feed_url(url)
|
||||
if @feed.present?
|
||||
if @feed.standalone_request_at.blank?
|
||||
FeedStatus.new.perform(@feed.id)
|
||||
FeedUpdate.new.perform(@feed.id)
|
||||
end
|
||||
else
|
||||
feeds = FeedFinder.feeds(url)
|
||||
@feed = feeds.first
|
||||
end
|
||||
|
||||
if @feed.present?
|
||||
@feed.touch(:standalone_request_at)
|
||||
else
|
||||
status_not_found
|
||||
end
|
||||
rescue => exception
|
||||
if Rails.env.production?
|
||||
ErrorService.notify(exception)
|
||||
status_not_found
|
||||
else
|
||||
raise exception
|
||||
end
|
||||
end
|
||||
|
||||
Even with this structure, I find all controllers easy to read and I
|
||||
think they can be easier to change.
|
||||
|
||||
Models
|
||||
|
||||
The app/models folders contain both ActiveRecord and normal Ruby
|
||||
objects. With few exceptions, they are not namespaced.
|
||||
|
||||
Jobs
|
||||
|
||||
The jobs folder contains Sidekiq jobs which are used to do processing
|
||||
on various objects. They are usually called from controllers and most
|
||||
of them are async.
|
||||
|
||||
Here is one job that is caching views:
|
||||
# https://github.com/feedbin/feedbin/blob/main/app/jobs/cache_entry_views.rb#L1
|
||||
|
||||
class CacheEntryViews
|
||||
include Sidekiq::Worker
|
||||
include SidekiqHelper
|
||||
|
||||
SET_NAME = "#{name}-ids"
|
||||
|
||||
def perform(entry_id, process = false)
|
||||
if process
|
||||
cache_views
|
||||
else
|
||||
add_to_queue(SET_NAME, entry_id)
|
||||
end
|
||||
end
|
||||
|
||||
def cache_views
|
||||
entry_ids = dequeue_ids(SET_NAME)
|
||||
entries = Entry.where(id: entry_ids).includes(feed: [:favicon])
|
||||
ApplicationController.render({
|
||||
partial: "entries/entry",
|
||||
collection: entries,
|
||||
format: :html,
|
||||
cached: true
|
||||
})
|
||||
ApplicationController.render({
|
||||
layout: nil,
|
||||
template: "api/v2/entries/index",
|
||||
assigns: {entries: entries},
|
||||
format: :html,
|
||||
locals: {
|
||||
params: {mode: "extended"}
|
||||
}
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
Presenters
|
||||
|
||||
There is a [61]BasePresenter and all other presenters are extending it
|
||||
via inheritance:
|
||||
|
||||
This controller defines a private method called presents:
|
||||
# https://github.com/feedbin/feedbin/blob/main/app/presenters/base_presenter.rb#
|
||||
L1
|
||||
|
||||
class BasePresenter
|
||||
def initialize(object, locals, template)
|
||||
@object = object
|
||||
@locals = locals
|
||||
@template = template
|
||||
end
|
||||
|
||||
# ...
|
||||
|
||||
private
|
||||
|
||||
def self.presents(name)
|
||||
define_method(name) do
|
||||
@object
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
and it is used like this for example:
|
||||
# https://github.com/feedbin/feedbin/blob/main/app/presenters/user_presenter.rb#
|
||||
L2
|
||||
|
||||
class UserPresenter < BasePresenter
|
||||
presents :user
|
||||
delegate_missing_to :user
|
||||
|
||||
# ... more code
|
||||
|
||||
def theme
|
||||
result = settings["theme"].present? ? settings["theme"] : nil
|
||||
result || user.theme || "auto"
|
||||
end
|
||||
# ... other code
|
||||
end
|
||||
|
||||
To use the presenters, there is a helper defined in ApplicationHelper
|
||||
will instantiate the proper helper based on the object class:
|
||||
module ApplicationHelper
|
||||
def present(object, locals = nil, klass = nil)
|
||||
klass ||= "#{object.class}Presenter".constantize
|
||||
presenter = klass.new(object, locals, self)
|
||||
yield presenter if block_given?
|
||||
presenter
|
||||
end
|
||||
|
||||
# more code ...
|
||||
end
|
||||
|
||||
and it is used [62]like this in views:
|
||||
<% present @user do |user_presenter| %>
|
||||
<% @class = "settings-body settings-#{params[:action]} theme-#{user_presente
|
||||
r.theme}"%>
|
||||
<% end %>
|
||||
|
||||
ApplicationComponents
|
||||
|
||||
Components are based on Phlex and they inherit from
|
||||
[63]ApplicationComponent
|
||||
|
||||
It defines a method to add Stimulus controller in components like this:
|
||||
# https://github.com/feedbin/feedbin/blob/main/app/views/components/application_
|
||||
component.rb#L25
|
||||
|
||||
def stimulus(controller:, actions: {}, values: {}, outlets: {}, classes: {}, d
|
||||
ata: {})
|
||||
stimulus_controller = controller.to_s.dasherize
|
||||
|
||||
action = actions.map do |event, function|
|
||||
"#{event}->#{stimulus_controller}##{function.camelize(:lower)}"
|
||||
end.join(" ").presence
|
||||
|
||||
values.transform_keys! do |key|
|
||||
[controller, key, "value"].join("_").to_sym
|
||||
end
|
||||
|
||||
outlets.transform_keys! do |key|
|
||||
[controller, key, "outlet"].join("_").to_sym
|
||||
end
|
||||
|
||||
classes.transform_keys! do |key|
|
||||
[controller, key, "class"].join("_").to_sym
|
||||
end
|
||||
|
||||
{ controller: stimulus_controller, action: }.merge!({ **values, **outlets, *
|
||||
*classes, **data})
|
||||
end
|
||||
|
||||
Where we can also see a bit of hash literal omission at {controller:
|
||||
stimulus_controller, action: }
|
||||
|
||||
But more interesting that this method that helps defining a Stimulus
|
||||
controller, is the method used to define a Stimulus item that uses
|
||||
binding to get variables from the object where it is used:
|
||||
# https://github.com/feedbin/feedbin/blob/main/app/views/components/application_
|
||||
component.rb#L47
|
||||
|
||||
def stimulus_item(target: nil, actions: {}, params: {}, data: {}, for:)
|
||||
stimulus_controller = binding.local_variable_get(:for).to_s.dasherize
|
||||
|
||||
action = actions.map do |event, function|
|
||||
"#{event}->#{stimulus_controller}##{function.to_s.camelize(:lower)}"
|
||||
end.join(" ").presence
|
||||
|
||||
params.transform_keys! do |key|
|
||||
:"#{binding.local_variable_get(:for)}_#{key}_param"
|
||||
end
|
||||
|
||||
defaults = { **params, **data }
|
||||
|
||||
if action
|
||||
defaults[:action] = action
|
||||
end
|
||||
|
||||
if target
|
||||
defaults[:"#{binding.local_variable_get(:for)}_target"] = target.to_s.came
|
||||
lize(:lower)
|
||||
end
|
||||
|
||||
defaults
|
||||
end
|
||||
|
||||
The part with binding does the following:
|
||||
* stimulus_controller =
|
||||
binding.local_variable_get(:for).to_s.dasherize This line retrieves
|
||||
the value of the local variable for, converts it to a string, and
|
||||
then applies the dasherize method (presumably to format it for use
|
||||
in a specific context, like a CSS class or an identifier in HTML).
|
||||
* Apparently binding.local_variable_get should not be needed as the
|
||||
variable is passed a keyword parameter to the method. But the name
|
||||
of the variable is for which is a reserved word and thus if the
|
||||
code would have been stimulus_controller = for.to_s_dasherize that
|
||||
would have raised syntax error, unexpected '.' (SyntaxError)
|
||||
|
||||
This is a way to have keyword arguments named as reserved words and
|
||||
still be able to use them.
|
||||
|
||||
ComponentsPreview
|
||||
|
||||
All components can be previewed via Lookbook and they can be found in
|
||||
test/components
|
||||
|
||||
Testing
|
||||
|
||||
For testing it uses Minitest, the default testing framework from Rails.
|
||||
It uses fixtures to set up the test db.
|
||||
|
||||
Tests are simple and direct, containing all preconditions and
|
||||
postconditions in each test. This is great for following what each test
|
||||
is doing.
|
||||
|
||||
There are controller tests, model tests, job tests and some system
|
||||
tests. There are more controller tests than system tests making the
|
||||
test suite run quite fast. Also the jobs are covered pretty good with
|
||||
testing as there is a log of logic in the jobs.
|
||||
|
||||
Custom assertions
|
||||
|
||||
There are some custom assertions created specifically to work with
|
||||
collections: assert_has_keys will check if all keys are included in the
|
||||
hash and assert_equal_ids will check if the two collections provided
|
||||
have the same ids (one being a collection of objects and the other one
|
||||
being a hash).
|
||||
# https://github.com/feedbin/feedbin/blob/main/test/support/assertions.rb#L3
|
||||
|
||||
def assert_has_keys(keys, hash)
|
||||
assert(keys.all? { |key| hash.key?(key) })
|
||||
end
|
||||
|
||||
def assert_equal_ids(collection, results)
|
||||
expected = Set.new(collection.map(&:id))
|
||||
actual = Set.new(results.map { |result| result["id"] })
|
||||
assert_equal(expected, actual)
|
||||
end
|
||||
|
||||
Conclusion
|
||||
|
||||
In conclusion, Feedbin is an open-source project that combines RSS
|
||||
feeds and newsletters into a convenient platform.
|
||||
|
||||
It utilizes Ruby on Rails, PostgreSQL, Sidekiq, and various other
|
||||
technologies to provide a robust and efficient service.
|
||||
|
||||
The code is well-organized and simple to follow the logic and what is
|
||||
happening. I think it will make it easy for anyone to contribute to
|
||||
this repo. If you want to run this yourself locally you should take a
|
||||
look at the [64]feedbin-docker.
|
||||
__________________________________________________________________
|
||||
|
||||
Enjoyed this article?
|
||||
|
||||
Join my [65]Short Ruby News newsletter for weekly Ruby updates from the
|
||||
community. For more Ruby learning resources, visit
|
||||
[66]rubyandrails.info. You can also find me on [67]Ruby.social or
|
||||
[68]Linkedin or [69]Twitter where I post mostly about Ruby and Rails.
|
||||
|
||||
Did you find this article valuable?
|
||||
|
||||
Support Lucian Ghinda by becoming a sponsor. Any amount is appreciated!
|
||||
(BUTTON) Sponsor
|
||||
[70]See recent sponsors | [71]Learn more about Hashnode Sponsors
|
||||
[72]Ruby[73]Ruby on Rails[74]Open Source[75]coding[76]Programming Blogs
|
||||
|
||||
References
|
||||
|
||||
Visible links:
|
||||
1. file:///?source=top_nav_blog_home
|
||||
2. https://hashnode.com/@lucianghinda
|
||||
3. https://hashnode.com/@lucianghinda
|
||||
4. https://allaboutcoding.ghinda.com/ruby-open-source-feedbin
|
||||
5. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-the-product
|
||||
6. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-open-source
|
||||
7. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-license
|
||||
8. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-technical-review
|
||||
9. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-ruby-and-rails-version
|
||||
10. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-architecture
|
||||
11. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-stats
|
||||
12. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-style-guide
|
||||
13. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-storage-persistence-and-in-memory-storage
|
||||
14. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-gems-used
|
||||
15. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-code-amp-design-patterns
|
||||
16. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-code-organisation
|
||||
17. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-routes
|
||||
18. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-controllers
|
||||
19. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-models
|
||||
20. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-jobs
|
||||
21. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-presenters
|
||||
22. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-applicationcomponents
|
||||
23. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-componentspreview
|
||||
24. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-testing
|
||||
25. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-custom-assertions
|
||||
26. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-conclusion
|
||||
27. https://feedbin.com/
|
||||
28. https://github.com/feedbin/feedbin/blob/main/LICENSE.md
|
||||
29. https://github.com/feedbin/feedbin/blob/main/LICENSE.md
|
||||
30. https://github.com/feedbin/rails
|
||||
31. https://github.com/Shopify/rails
|
||||
32. https://github.com/feedbin/rails/tree/7-1-stable-invalid-cache-entries
|
||||
33. https://github.com/feedbin/feedbin/tree/main/app/views/components
|
||||
34. https://github.com/feedbin/feedbin/blob/main/Gemfile#L38
|
||||
35. https://github.com/feedbin/feedbin/tree/main/app/assets/javascripts/web
|
||||
36. https://github.com/feedbin/feedbin/blob/abf1ad883dab8a3464fe12e4653de6323296175b/config/importmap.rb#L1
|
||||
37. https://github.com/feedbin/feedbin/blob/abf1ad883dab8a3464fe12e4653de6323296175b/Gemfile#L66
|
||||
38. https://github.com/feedbin/feedbin/blob/abf1ad883dab8a3464fe12e4653de6323296175b/Gemfile#L94
|
||||
39. https://github.com/feedbin/feedbin/blob/main/db/structure.sql
|
||||
40. https://github.com/feedbin/feedbin/blob/abf1ad883dab8a3464fe12e4653de6323296175b/config/initializers/redis.rb#L1
|
||||
41. https://github.com/feedbin/feedbin/blob/main/app/models/redis_lock.rb#L1
|
||||
42. https://github.com/feedbin/feedbin/blob/main/lib/clock.rb#L8
|
||||
43. https://github.com/pauldix/sax-machine
|
||||
44. https://github.com/feedjira/feedjira
|
||||
45. https://github.com/feedbin/html-pipeline
|
||||
46. https://github.com/ostinelli/apnotic
|
||||
47. https://github.com/ai/autoprefixer-rails
|
||||
48. https://github.com/Rykian/clockwork
|
||||
49. https://github.com/janko/down
|
||||
50. https://github.com/phlex-ruby/phlex-rails
|
||||
51. https://github.com/fphilipe/premailer-rails
|
||||
52. https://rubygems.org/gems/raindrops
|
||||
53. https://github.com/ankane/strong_migrations
|
||||
54. https://github.com/pushpad/web-push
|
||||
55. https://github.com/stripe-ruby-mock/stripe-ruby-mock
|
||||
56. https://github.com/rails/rails-controller-testing
|
||||
57. https://github.com/feedbin/feedbin/blob/main/Gemfile
|
||||
58. https://github.com/feedbin/feedbin/blob/main/lib/conditional_sass_compressor.rb#L1
|
||||
59. https://github.com/feedbin/feedbin/blob/main/config/routes.rb#L133
|
||||
60. https://github.com/feedbin/feedbin/blob/main/app/views/entries/destroy.js.erb#L1
|
||||
61. https://github.com/feedbin/feedbin/blob/main/app/presenters/base_presenter.rb#L1
|
||||
62. https://github.com/feedbin/feedbin/blob/main/app/views/layouts/settings.html.erb#L1
|
||||
63. https://github.com/feedbin/feedbin/blob/main/app/views/components/application_component.rb#L3
|
||||
64. https://github.com/angristan/feedbin-docker
|
||||
65. https://shortruby.com/
|
||||
66. http://rubyandrails.info/
|
||||
67. http://Ruby.social/
|
||||
68. https://linkedin.com/in/lucianghinda
|
||||
69. https://x.com/lucianghinda
|
||||
70. file:///sponsor
|
||||
71. https://hashnode.com/sponsors
|
||||
72. file:///tag/ruby?source=tags_bottom_blogs
|
||||
73. file:///tag/ruby-on-rails?source=tags_bottom_blogs
|
||||
74. file:///tag/opensource?source=tags_bottom_blogs
|
||||
75. file:///tag/coding?source=tags_bottom_blogs
|
||||
76. file:///tag/programming-blogs?source=tags_bottom_blogs
|
||||
|
||||
Hidden links:
|
||||
78. file://localhost/
|
||||
79. file://localhost/?source=top_nav_blog_home
|
||||
80. https://twitter.com/lucianghinda
|
||||
81. https://github.com/lucianghinda
|
||||
82. https://shortruby.com/
|
||||
83. https://hashnode.com/@lucianghinda
|
||||
84. https://app.daily.dev/lucianghinda
|
||||
85. https://linkedin.com/in/lucianghinda
|
||||
86. https://ruby.social/@lucian
|
||||
87. file://localhost/rss.xml
|
||||
88. https://feedbin.com/about
|
||||
89. https://github.com/feedbin/feedbin/blob/main/LICENSE.md
|
||||
90. http://rubyandrails.info/
|
||||
91. https://ruby.social/@lucian
|
||||
Reference in New Issue
Block a user