847 lines
27 KiB
Plaintext
847 lines
27 KiB
Plaintext
[2]
|
||
|
||
[4]
|
||
[]Lucian Ghinda
|
||
All about coding
|
||
|
||
Follow
|
||
|
||
[7]All about coding
|
||
|
||
Follow
|
||
[8][9][10][11][12][13][14][15]
|
||
Ruby open source: feedbinRuby open source: feedbin
|
||
|
||
Ruby open source: feedbin
|
||
|
||
[16][]Lucian Ghinda's photoLucian Ghinda's photo
|
||
[17]Lucian Ghinda
|
||
·[18]Nov 17, 2023·
|
||
|
||
11 min read
|
||
|
||
Table of contents
|
||
|
||
• [19]
|
||
The product
|
||
• [20]
|
||
Open source
|
||
□ [21]
|
||
License
|
||
• [22]
|
||
Technical review
|
||
□ [23]
|
||
Ruby and Rails version
|
||
□ [24]
|
||
Architecture
|
||
□ [25]
|
||
Stats
|
||
□ [26]
|
||
Style Guide
|
||
□ [27]
|
||
Storage, Persistence and in-memory storage
|
||
□ [28]
|
||
Gems used
|
||
□ [29]
|
||
Code & Design Patterns
|
||
☆ [30]
|
||
Code Organisation
|
||
☆ [31]
|
||
Routes
|
||
☆ [32]
|
||
Controllers
|
||
☆ [33]
|
||
Models
|
||
☆ [34]
|
||
Jobs
|
||
☆ [35]
|
||
Presenters
|
||
☆ [36]
|
||
ApplicationComponents
|
||
☆ [37]
|
||
ComponentsPreview
|
||
• [38]
|
||
Testing
|
||
□ [39]
|
||
Custom assertions
|
||
• [40]
|
||
Conclusion
|
||
|
||
The product
|
||
|
||
[41]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"
|
||
|
||
[42][dcaac2f3-3]
|
||
|
||
Open source
|
||
|
||
The open-source repository can be found at [43]https://github.com/feedbin/
|
||
feedbin
|
||
|
||
License
|
||
|
||
The [44]license they use is MIT:
|
||
|
||
[45][26c3731c-c]
|
||
|
||
Technical review
|
||
|
||
Ruby and Rails version
|
||
|
||
They are currently using:
|
||
|
||
• Ruby version 3.2.2
|
||
|
||
• They used a fork of Rails at [46]https://github.com/feedbin/rails forked
|
||
from [47]https://github.com/Shopify/rails. They are using a branch called
|
||
[48]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 [49]components
|
||
|
||
• They are using [50]Jquery for the JS library
|
||
|
||
• They have some custom JS code written in [51]CoffeeScript
|
||
|
||
• They are using Hotwire via [52]importmaps
|
||
|
||
• They are using [53]Tailwind
|
||
|
||
Stats
|
||
|
||
Running /bin/rails stats will output the following:
|
||
|
||
[12169b38-4]
|
||
|
||
Running VSCodeCounter will give the following stats:
|
||
|
||
[99f9ce55-5]
|
||
|
||
Style Guide
|
||
|
||
For Ruby:
|
||
|
||
• They are using [54]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 [55]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 statistics 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 (UUIDs)';
|
||
|
||
Redis is configured to be used with Sidekiq.
|
||
|
||
This is what the [56]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 [57]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: true) }
|
||
end
|
||
end
|
||
|
||
Further down this is used in a [58]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:
|
||
|
||
• [59]sax-machine - "A declarative sax parsing library backed by Nokogiri"
|
||
|
||
• [60]feedjira - "Feedjira is a Ruby library designed to parse feeds"
|
||
|
||
• [61]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"
|
||
|
||
• [62]apnotic - "A Ruby APNs HTTP/2 gem able to provide instant feedback"
|
||
|
||
• [63]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"
|
||
|
||
• [64]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"
|
||
|
||
• [65]down - "Streaming downloads using net/http, http.rb, HTTPX or wget"
|
||
|
||
• [66]phlex-rails - "Phlex is a framework that lets you compose web views in
|
||
pure Ruby"
|
||
|
||
• [67]premailer-rails - "This gem is a drop in solution for styling HTML
|
||
emails with CSS without having to do the hard work yourself"
|
||
|
||
• [68]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"
|
||
|
||
• [69]strong_migrations - "Catch unsafe migrations in development"
|
||
|
||
• [70]web-push - "This gem makes it possible to send push messages to web
|
||
browsers from Ruby backends using the Web Push Protocol"
|
||
|
||
• [71]stripe-ruby-mock - "A drop-in library to test stripe without hitting
|
||
their servers"
|
||
|
||
• [72]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 [73]
|
||
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: "feedbin"
|
||
gem "carrierwave", github: "feedbin/carrierwave", branch: "feedbin"
|
||
gem "sax-machine", github: "feedbin/sax-machine", branch: "feedbin"
|
||
gem "feedjira", github: "feedbin/feedjira", branch: "f2"
|
||
gem "feedkit", github: "feedbin/feedkit", branch: "master"
|
||
gem "html-pipeline", github: "feedbin/html-pipeline", branch: "feedbin"
|
||
gem "html_diff", github: "feedbin/html_diff", ref: "013e1bb"
|
||
gem "twitter", github: "feedbin/twitter", branch: "feedbin"
|
||
|
||
# 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 [74]ConditionalSassCompressor
|
||
|
||
# https://github.com/feedbin/feedbin/blob/main/lib/conditional_sass_compressor.rb#L1
|
||
|
||
class ConditionalSassCompressor
|
||
def compress(string)
|
||
return string if string =~ /tailwindcss/
|
||
options = { syntax: :scss, cache: false, read_cache: false, style: :compressed}
|
||
begin
|
||
Sprockets::Autoload::SassC::Engine.new(string, options).render
|
||
rescue => e
|
||
puts "Could not compress '#{string[0..65]}'...: #{e.message}, skipping compression"
|
||
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 [75]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_controller.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 [76]destroy.js.erb :
|
||
|
||
$('[data-behavior~=entries_target] [data-entry-id=<%= @entry.id %>]').remove();
|
||
|
||
feedbin.Counts.get().removeEntry(<%= @entry.id %>, <%= @entry.feed_id %>, 'unread')
|
||
feedbin.Counts.get().removeEntry(<%= @entry.id %>, <%= @entry.feed_id %>, 'starred')
|
||
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_controller.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/feeds_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 [77]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 [78]like this in views:
|
||
|
||
<% present @user do |user_presenter| %>
|
||
<% @class = "settings-body settings-#{params[:action]} theme-#{user_presenter.theme}"%>
|
||
<% end %>
|
||
|
||
ApplicationComponents
|
||
|
||
Components are based on Phlex and they inherit from [79]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: {}, data: {})
|
||
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.camelize(: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 [80]
|
||
feedbin-docker.
|
||
|
||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
|
||
Enjoyed this article?
|
||
|
||
👉 Join my [81]Short Ruby News newsletter for weekly Ruby updates from the
|
||
community and visit [82][83]rubyandrails.info, a directory with learning
|
||
content about Ruby.
|
||
|
||
👐 Subscribe to my Ruby and Ruby on rails courses over email at [84]
|
||
learn.shortruby.com - effortless learning anytime, anywhere
|
||
|
||
🤝 Let's connect on [85][86]Ruby.social or [87]Linkedin or [88]Twitter where I
|
||
post mainly about Ruby and Rails.
|
||
|
||
🎥 Follow me on [89]my YouTube channel for short videos about Ruby
|
||
|
||
Did you find this article valuable?
|
||
|
||
Support Lucian Ghinda by becoming a sponsor. Any amount is appreciated!
|
||
|
||
Sponsor
|
||
[91]See recent sponsors | [92]Learn more about Hashnode Sponsors
|
||
[93]Ruby[94]Ruby on Rails[95]Open Source[96]coding[97]Programming Blogs
|
||
|
||
References:
|
||
|
||
[2] https://allaboutcoding.ghinda.com/
|
||
[4] https://allaboutcoding.ghinda.com/?source=top_nav_blog_home
|
||
[7] https://allaboutcoding.ghinda.com/?source=top_nav_blog_home
|
||
[8] https://twitter.com/lucianghinda
|
||
[9] https://github.com/lucianghinda
|
||
[10] https://shortruby.com/
|
||
[11] https://hashnode.com/@lucianghinda
|
||
[12] https://app.daily.dev/lucianghinda
|
||
[13] https://linkedin.com/in/lucianghinda
|
||
[14] https://ruby.social/@lucian
|
||
[15] https://allaboutcoding.ghinda.com/rss.xml
|
||
[16] https://hashnode.com/@lucianghinda
|
||
[17] https://hashnode.com/@lucianghinda
|
||
[18] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin
|
||
[19] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-the-product
|
||
[20] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-open-source
|
||
[21] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-license
|
||
[22] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-technical-review
|
||
[23] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-ruby-and-rails-version
|
||
[24] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-architecture
|
||
[25] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-stats
|
||
[26] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-style-guide
|
||
[27] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-storage-persistence-and-in-memory-storage
|
||
[28] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-gems-used
|
||
[29] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-code-amp-design-patterns
|
||
[30] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-code-organisation
|
||
[31] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-routes
|
||
[32] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-controllers
|
||
[33] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-models
|
||
[34] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-jobs
|
||
[35] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-presenters
|
||
[36] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-applicationcomponents
|
||
[37] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-componentspreview
|
||
[38] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-testing
|
||
[39] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-custom-assertions
|
||
[40] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-conclusion
|
||
[41] https://feedbin.com/
|
||
[42] https://feedbin.com/about
|
||
[43] https://github.com/feedbin/feedbin/blob/main/LICENSE.md
|
||
[44] https://github.com/feedbin/feedbin/blob/main/LICENSE.md
|
||
[45] https://github.com/feedbin/feedbin/blob/main/LICENSE.md
|
||
[46] https://github.com/feedbin/rails
|
||
[47] https://github.com/Shopify/rails
|
||
[48] https://github.com/feedbin/rails/tree/7-1-stable-invalid-cache-entries
|
||
[49] https://github.com/feedbin/feedbin/tree/main/app/views/components
|
||
[50] https://github.com/feedbin/feedbin/blob/main/Gemfile#L38
|
||
[51] https://github.com/feedbin/feedbin/tree/main/app/assets/javascripts/web
|
||
[52] https://github.com/feedbin/feedbin/blob/abf1ad883dab8a3464fe12e4653de6323296175b/config/importmap.rb#L1
|
||
[53] https://github.com/feedbin/feedbin/blob/abf1ad883dab8a3464fe12e4653de6323296175b/Gemfile#L66
|
||
[54] https://github.com/feedbin/feedbin/blob/abf1ad883dab8a3464fe12e4653de6323296175b/Gemfile#L94
|
||
[55] https://github.com/feedbin/feedbin/blob/main/db/structure.sql
|
||
[56] https://github.com/feedbin/feedbin/blob/abf1ad883dab8a3464fe12e4653de6323296175b/config/initializers/redis.rb#L1
|
||
[57] https://github.com/feedbin/feedbin/blob/main/app/models/redis_lock.rb#L1
|
||
[58] https://github.com/feedbin/feedbin/blob/main/lib/clock.rb#L8
|
||
[59] https://github.com/pauldix/sax-machine
|
||
[60] https://github.com/feedjira/feedjira
|
||
[61] https://github.com/feedbin/html-pipeline
|
||
[62] https://github.com/ostinelli/apnotic
|
||
[63] https://github.com/ai/autoprefixer-rails
|
||
[64] https://github.com/Rykian/clockwork
|
||
[65] https://github.com/janko/down
|
||
[66] https://github.com/phlex-ruby/phlex-rails
|
||
[67] https://github.com/fphilipe/premailer-rails
|
||
[68] https://rubygems.org/gems/raindrops
|
||
[69] https://github.com/ankane/strong_migrations
|
||
[70] https://github.com/pushpad/web-push
|
||
[71] https://github.com/stripe-ruby-mock/stripe-ruby-mock
|
||
[72] https://github.com/rails/rails-controller-testing
|
||
[73] https://github.com/feedbin/feedbin/blob/main/Gemfile
|
||
[74] https://github.com/feedbin/feedbin/blob/main/lib/conditional_sass_compressor.rb#L1
|
||
[75] https://github.com/feedbin/feedbin/blob/main/config/routes.rb#L133
|
||
[76] https://github.com/feedbin/feedbin/blob/main/app/views/entries/destroy.js.erb#L1
|
||
[77] https://github.com/feedbin/feedbin/blob/main/app/presenters/base_presenter.rb#L1
|
||
[78] https://github.com/feedbin/feedbin/blob/main/app/views/layouts/settings.html.erb#L1
|
||
[79] https://github.com/feedbin/feedbin/blob/main/app/views/components/application_component.rb#L3
|
||
[80] https://github.com/angristan/feedbin-docker
|
||
[81] https://shortruby.com/
|
||
[82] http://rubyandrails.info/
|
||
[83] http://rubyandrails.info/
|
||
[84] https://learn.shortruby.com/
|
||
[85] https://ruby.social/@lucian
|
||
[86] http://ruby.social/
|
||
[87] https://linkedin.com/in/lucianghinda
|
||
[88] https://x.com/lucianghinda
|
||
[89] https://www.youtube.com/@shortruby
|
||
[91] https://allaboutcoding.ghinda.com/sponsor
|
||
[92] https://hashnode.com/sponsors
|
||
[93] https://allaboutcoding.ghinda.com/tag/ruby?source=tags_bottom_blogs
|
||
[94] https://allaboutcoding.ghinda.com/tag/ruby-on-rails?source=tags_bottom_blogs
|
||
[95] https://allaboutcoding.ghinda.com/tag/opensource?source=tags_bottom_blogs
|
||
[96] https://allaboutcoding.ghinda.com/tag/coding?source=tags_bottom_blogs
|
||
[97] https://allaboutcoding.ghinda.com/tag/programming-blogs?source=tags_bottom_blogs
|