Ruby
Useful Resources
- Gemfile of Dreams - The Libraries Evil Martians Use to Build Rails Apps
- Using Zeitwerk Outside Rails - Zeitwerk is a thread-safe Ruby code loader supporting both autoloading and eager loading. It’s commonly associated with Rails, but can be used without it too.
- Railway design
- Dry Monads - Very interesting and popular concept of
dryin Ruby, however it adds a substantial mental strain when working with Rails concepts as you need to switch between them / see the difference. - Graceful Dev Avdi Grim with a lot of high quality free and paid content
Articles
blog has information on why to prefer accessing instance variables via attribute methods.
TODO
irb --simple-prompt
self.public_methods - Object.public_methods
Debugging
if $PROGRAM_NAME == __FILE__
binding.irb if $DEBUG
end
ruby -d series.rb #
# or
ruby --disable-gems -d series.rb
load script into irb session(instead of adding binding at the EOF)
irb --simple-prompt -r ./series.rb
RDBG
bundle exec rdbg -c -- bundle exec rspec spec/worker_integrations/async_jobs/events/create_customer_order_from_external_order_updated_event_integration_spec.rb:57
b ActiveRecord::Associations::SingularAssociation#build
? b # RTFM
c # continue
i # info
s # step
f 1 # step back? frame command
del 0 # delete breakpoint
l # list, similar whereami?
finish
class instance vars
class Lala
def self.lala
@hey ||= Time.now
end
end
Lala.lala
14:17:21.110873 +1000
Lala.lala
14:17:21.110873 +1000
Load Path
$LOAD_PATH.unshift(File.join(__dir__, 'lib'))
$LOAD_PATH.unshift(File.expand_path('../lib', __FILE__))
Case pattern matching
case input
in "lala" then puts 'yolo'
in [_,a] if true
puts a #=> 2
in _ => anything_else_reassigned
puts anything_else_reassigned
end
Rails Resources
- Advanced Active Record Concepts - A tour of concepts including locking records to avoid conflicts, using UUIDs as primary keys, fulltext search, using database views, and working with geospatial data. I suspect it might end up with a 2024 update for vector similarity queries.. 😁
- Split your database seeds.rb by Rails environment
Tools
- An OpenAI API client.
- twilio-ruby
- Bullet Train Rails Template
- rails-brotli-cache
- rspec-sidekiq
- deep_pluck - Pluck deeply into nested associations without loading a bunch of records.
Gems
Cleaning up
gem pristine --all
gem cleanup
- Gemfile of Dreams - The Libraries Evil Martians Use to Build Rails Apps
Scheduling
https://github.com/jjb/ruby-clock - ruby clock (for simple tasks)
Money
257.78 * 100
#=> 25777.999999999996
289.15 * 100
#=> 28914.999999999996
require "bigdecimal"
"%2.2f" % (BigDecimal('289.15') * BigDecimal(100))
#=> "28915.00"
https://www.honeybadger.io/blog/ruby-currency/
Ruby
assessment_type&.name
# OR
assessment_type.try(:name) # ------> RAILS verion
ARGS forwarding
def concrete_method(*positional_args, **keyword_args, &block)
positional_args
keyword_args
block.call
end
def forwarding_method(...)
concrete_method(...)
end
concrete_method(1, b: 2) { puts 3 }
All class methods
self.class.instance_methods(false)
.each { |m| self.send m }
UUID
Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, "fresho_salmon_heads")
Rake
https://github.com/friendlyantz/rake-sandbox
Inline execution
ruby -e puts 'Hello, world!'
rake -e puts 'Hello, world!'
File writing
%W[ch1.md ch2.md ch3.md].each do |md_file|
html_file = File.basename(md_file, ".md") + ".html"
file html_file => md_file do
sh "pandoc -o #{html_file} #{md_file}"
end
end
I had to call with .html, not .md, which was a bit confusing.
Now rake handles file writing and trackes if there are any updates in .md file worth calling a conversion operation
❯ rake md_to_html README.html
pandoc -o README.html README.md
❯ rake md_to_html README.html
❯ rake md_to_html README.html
using Rules
unlike previous commit this rule defines a saving
RULE for files with .html extension with prerequisite ‘md’ file, so the
rake initially looks for a rule with exact match of README.html, doesn’t
find and then goes to generic ‘.html’ rule
Previously with used file task, with explicit name of the file
rule '.html' => '.md' do |t|
if `which pandoc`.empty?
puts 'pandoc is not installed'
exit 1
end
sh "pandoc -o #{t.name} #{t.source}"
end
Listing files
files = Rake::FileList['**/*.md']
files.exclude('**/~*') # exclude files with ~ in the name
files.exclude do |file|
`git ls-files #{file}`.empty? # exclude files that are not tracked by git
end
files.exclude /^ignoredir/ # using REGEX
OR create instance of a file list and pass a block
files = Rake::FileList.new('**/*.md') do |fl|
fl.exclude('**/~*') # exclude files with ~ in the name
fl.exclude do |file|
`git ls-files #{file}`.empty? # exclude files that are not tracked by git
end
fl.exclude(/^ignoredir/) # using REGEX
fl.exclude(/README/) # using REGEX
end
listing files with new file ext
files.ext('html')
Exercism takeaways
GitHub exercism/ruby solutions with some comments in commit Interesting exercises:
- Two Fer
- Resistor Color Duo
class vs instance methods
The class level method is there for convenience only, and it should stay inflexible, as it is there for convenience not power. We will see this decision in use in our “expansion”, below.
Hash freezing and I18N connection
The languages available and the translations available should be a single thing, a Hash. Otherwise there more of a chance that things can become disconnected in the way that they are now related, (but not connected).
then as a circuit breaker
# meets condition, no-op
1.then.detect(&:odd?) # => 1
# does not meet condition, drop value
2.then.detect(&:odd?) # => nil
Progress Bar with custom style
bundle add progressbar
require 'progressbar'
progressbar = ProgressBar.create(
total: Ladida.count,
format: "%a %e %P% %b\u{15E7}%i RateOfChange: %r Processed: %c from %C",
progress_mark: " ",
remainder_mark: "\u{FF65}",
)
# in loop just do
progressbar.increment
# with colorize
require 'colorize'
progressbar = ProgressBar.create(
total: range.count,
format: "%a %e %P% %b#{"\u{15E7}".yellow}%i RateOfChange: %r Processed: %c from %C",
progress_mark: ' ',
remainder_mark: "\u{FF65}".light_green
)
docs https://github.com/jfelchner/ruby-progressbar/wiki/Formatting
CSV + AWS S3
require "csv"
require "aws-sdk-s3"
s3 = Aws::S3::Client.new(
region: "ap-southeast-2",
credentials: Aws::Credentials.new(
ENV.fetch("AWS_ACCESS_KEY_ID"),
ENV.fetch("AWS_SECRET_ACCESS_KEY"),
),
)
env = "stg"
response = s3.get_object(
{
bucket: "bucket_name#{env}",
key: "dir/filename.csv",
},
)
input_csv = CSV.new(response.body, headers: true)
progressbar = ProgressBar.create(total: input_csv.count)
input_csv.rewind # progress bar kills csv data
modified_data = []
input_csv.each do |row|
row["new_column_header"] = "value_for_this_line"
modified_data << row
progressbar.increment
end
CSV.open("tmp/modified_data.csv", "wb") do |csv|
csv << modified_data.first.headers
modified_data.each do |row|
csv << row
end
end
s3.put_object(
{
body: File.read("tmp/modified_data.csv"),
bucket: "bucker_name#{env}",
key: "dir/filename_saturated.csv",
},
)
Linting
https://evilmartians.com/chronicles/rubocoping-with-legacy-bring-your-ruby-code-up-to-standard
Rails
init new project
rails new myapp \
--minimal \
--database=postgresql
Search all database
ApplicationRecord.descendants.map {|m| m.all }
Views
https://www.phlex.fun/
Ignore columns on load
self.ignored_columns += %w[some_id]
Rails Logging switching via console
Rails.logger.level = :debug
1. Layered Rails - as a Web Application Framework
good abstraction layer:
- An abstraction should have a single responsibility. However, the responsibilities themselves can be broad but should not overlap (thus, following the separation of concerns principle).
- Layers should be loosely coupled and have no circular or reverse dependencies. we should try to minimize the number of connections between layers.
- Abstractions should not leak their internals.
- it should be possible to test abstractions in isolation.
abstraction layer examples(not layered architecture - see ch5):
- HTTP pre-/post-processing layer: Rails middleware
Middleware is a component that wraps a core unit (function) execution and can inspect and modify input and output data without changing its interface.
- Routing Layers.
However, health check endpoint can be seen as a property of a Rack app(middleware)
- External Inbound Abstraction Layer: Controllers, as well as Action Cable channels or Action Mailbox mailboxes.
- Internal Inbound Abstraction Layer: Background jobs
- Database Layers
- other: model, view, etc
2. Layered Rails - Active Models and Records
Validations are for humans; constraints are for machines.
3. Layered Rails - Adapters
adapter pattern - activeStorage plug in pattern - the core system provides the extension points for plugins to hook into
The key difference between adapters and plugins is that plugins provide additional functionality, not just an expected interface.
The wrapper pattern could be seen as a degenerate case of the adapter pattern. With a wrapper object, we have both an application-level interface and the implementation encapsulation. Wrappers are usually much easier to deal with in tests than implementations.
4. Layered Rails - Rails Anti-Patterns
Callbacks - hidden dependency
- unlike plugins, callbacks don’t have to implement a particular interface, and they have no limits, neither technically nor conceptually.
p ActiveRecord::Callbacks::CALLBACKS
#=> [:after_initialize, :after_find, :after_touch, :before_validation, :after_validation, :before_save, :around_save, :after_save, :before_create, :around_create, :after_create, :before_update, :around_update, :after_update, :before_destroy, :around_destroy, :after_destroy, :after_commit, :after_rollback]
types of callbacks
Transformers and utility callbacks (✅good to keep in models 4-5/5)
transformer
before_validation :compute_shortname, on: :create
normalization callback –(✅good to keep in models)
before_validation :squish_content, if: :content_changed?
# but rails gives:
class Post < ApplicationRecord
normalizes :content, with: -> { _1.squish }
end
denormalization (✅good to keep in models)
before_save :set_word_count, if: :content_changed?
technical/utility callbacks (✅good to keep in models) i.e. caching posts count ```ruby belongs_to :post, touch: true, counter_cache: true # or explicitly
belongs_to :post after_save { post.touch }
after_create do Post.increment_counter(:comments_count, post_id) end
after_destroy do Post.decrement_counter(:comments_count, post_id) end ```
Operations and event handlers(🛑avoid if possible 1-2/5)
after_create :generate_initial_project, unless: :admin?
after_commit :send_welcome_email, on: :create
after_commit :send_analytics_event, on: :create, if: :tracking_consent?
after_commit :sync_with_crm
use event drivent architecture instead
Rails concerns
types of modules:
- Behavior
- Builder
- Static methods collection
- Namespace concerns are modules with extras:
- Concerns provide a DSL to simplify injecting standard Rails operations (defining callbacks, associations, and so on)
- Concerns support dependency resolution for included modules
TIP: extract not code but behaviors. delegate object. value object
global vs current state
- Global state introduces hidden dependencies between application components (and abstraction layers).
- Mutable global state makes code execution unpredictable, since it can be changed outside the current context. In multithreaded environments, that can lead to bugs due to race conditions.
- Understanding and testing code relying on globals is more complicated. but it’s is ok if done wisely
Current attributes have three design flaws:
- Values can be written and read from anywhere, and no ceremony is required
- Reading unset values is possible (the result value would be nil)
- The same attribute can be written multiple times during the lifetime of the execution context
rules to enforce:
- Keep the number of Current attributes as small as possible
- Always write attributes once within the same execution context
- Only write within the small number of abstraction layers (for example, only inbound layers)
- Only read within the small number of abstraction layers (and never from models)
5. Layered Rails - Rails Abstractions Are Not Enough
controller layer is an external inbound layer. controller layer wraps all other layers in the application, being an entry point for user actions. Controllers’ primary responsibilities are building execution contexts (for example, authentication) and transforming Rack requests into business actions.
How do we evaluate the maintainability of this code?
- churn complexity
- test complexity
Service objects
between controller and model Having a base class with a common interface and utilities is the first thing you need to do:
class ApplicationService extend Dry::Initializer def self.call(...) = new(...).call endThat will help you to keep service objects’ style uniform and simplify adding extensions in the future (for example, logging or instrumentation features)
Layered architecture is an established term for the architectural pattern, which implies the separation of application components/functions into horizontal logical layers.
four-layer architecture typical for applications following the domain-driven design (DDD) paradigm: 1. Presentation layer: Responsible for handling user interactions and presenting the information to users (via the UI). 2. Application layer: Organizes domain objects to fulfill required use cases. 3. Domain layer: Describes entities, rules, invariants, and so on. This layer maintains the state of the application. 4. Infrastructure layer: Consists of supporting technologies (databases, frameworks, API clients, and so on). ![[Pasted image 20251228195550.png]] keep the number of links between layers small. However, we can hit the architecture sinkhole problem.
every abstraction layer must belong to a single architecture layer.
![[Pasted image 20251228195854.png]]
Part II: Extracting Layers from Models
6. Layered Rails - Data Layer Abstractions
scopes carry semantical meaning; scopes are like query objects, but not all scopes are like that.
class Post < ApplicationRecord
scope :ordered, -> { order(created_at: :desc) }
scope :published, -> { where(draft: false) }
scope :kept, -> { where(deleted_at: nil) }
scope :previous_week, -> {
where(created_at: Date.current.prev_week.all_week)
}
end
Extracting queries into models can help with code deduplication and better isolation, but it turns the model class into a God object. i.e. ```ruby Post.kept.published.ordered class User < ApplicationRecord
def self.with_bookmarked_posts(period = :previous_week)
bookmarked_posts =
Post.kept.public_send(period)
.where.associated(:bookmarks)
.select(:user_id).distinct
with(bookmarked_posts:).joins(:bookmarked_posts)
end end
Now we can use this query as follows
User.with_bookmarked_posts
SO START WITH
```ruby
class ApplicationQuery
private attr_reader :relation
def initialize(relation) = @relation = relation
def resolve(...) = relation
end
# evolving to
class ApplicationQuery
class << self
def resolve(...) = new.resolve(...)
end
end
and query object
class UserWithBookmarkedPostsQuery < ApplicationQuery
def initialize(relation = User.all) = super(relation) # later added cherry on top
def resolve(period: :previous_week)
bookmarked_posts = build_bookmarked_posts_scope(period)
relation
.with(bookmarked_posts:)
.joins(:bookmarked_posts)
end
private
def build_bookmarked_posts_scope(period)
return Post.none unless Post.respond_to?(period)
Post.public_send(period)
.where.associated(:bookmarks)
.select(:user_id).distinct
end
end
# usages
UserWithBookmarkedPostsQuery
.new(User.all)
.resolve(period: :previous_month).where(name: "Vova")
# or after added initializer
UserWithBookmarkedPostsQuery.new.resolve
BUT WAIT, THERE IS MORE 🚀
class ApplicationQuery
class << self
def query_model
name.sub(/::[^\:]+$/, "").safe_constantize
end
alias_method :call, :resolve # this is for Rails Query object compatibility with scopes, since query objects are less readable than scopes
def resolve(...) = new.resolve(...)
end
def initialize(relation = self.class.query_model.all)
@relation = relation
end
end
#####
class Post::DraftsQuery < ApplicationQuery
def resolve = relation.where(draft: true)
end
Post::DraftsQuery.resolve #== Post.all.where(draft: true)
Scopes versus query objects
using alias hack above, we can:
class User < ApplicationRecord
scope :with_bookmarked_posts, WithBookmarkedPostsQuery
end
account.users.with_bookmarked_posts
Reusable query objects and Arel
…..
6. Layered Rails - Representation Layer
helpers are global.
A presenter is an object that encapsulates another object (or multiple objects) to provide an interface (representation) for the view layer
A decorator wraps a given object and adds new behavior to it dynamically without affecting other instances of a given class or creating new classes. The decorated object’s interface is a superset of the original interface;
In Ruby, we have a built-in mechanism to create decorators—SimpleDelegator
in the Rails community, the word decorator is usually used to describe presenting decorators or presenters acting as decorators.
OPEN PRESENTERS allow method calls to pass through and reach the target object, while CLOSED presenters do not
it can be reasonable to start with decorating presenters, so you can gradually extract presentation logic from models. For new UI logic, it makes sense to use stricter closed presenters
….
9. Layered Rails - Auth
kinds of protection
- authentication
- authorization
- system constraints
- and validations
Authorization rules must describe your business logic.Only authorization enforcement, the act of performing authorization, must stay in the presentation layer, and the enforcement must rely on the rules defined lower in the architecture stack.
authorization rules are not part of the domain layer. Domain objects do not need authorization; they live in the authorized context.
Classic authorization models:
- Role-based access control - RBAC (aka DAC, MAC)
- Roles can be static (a fixed set) or dynamic (created by users)
- Roles can be backed by a model or just an attribute in the User model
- permissions always exist in a role-based authorization model,
- A typical RBAC model problem is role explosion
- Attribute-based access control (ABAC), aka policy-based access control (PBAC).
- The flexibility of the ABAC model is limited only by the expressiveness of the language we use to define authorization rules—that is, the rules and the corresponding code can be as sophisticated as we can imagine. Keeping access rules logic in an inbound layer (such as controllers) quickly leads to duplication and overall higher complexity of both the application and test code.
Putting authorization rules into models can look attractive. but issues are:
- such methods are not context- aware; we should either add separate, context-specific methods or try to add modifying parameters, thus increasing the complexity of the
#can?method. - Authorization rules are not part of the domain layer. Domain objects do not need authorization; they live in the authorized context.
Performance implications of authorization
The first option is to leverage common preloading techniques (#preload, #eager_load, and so on.)
🚀An alternative approach to resolving N+1 authorization is to add caching.
10. Layered Rails - Notifications Layer
The responsibilities of this layer are as listed here:
- Deciding which communication channels to use for a given notification and a user
- Preparing notification payloads (email subjects and bodies, SMS contents, and so on)
-
Interacting with delivery services (mailing servers, third-party APIs, and so on)
…
Leave a comment