Useful Resources

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

Tools

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
    end
    

    That 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:

  1. 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.
  2. 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:

  1. Deciding which communication channels to use for a given notification and a user
  2. Preparing notification payloads (email subjects and bodies, SMS contents, and so on)
  3. Interacting with delivery services (mailing servers, third-party APIs, and so on)


Leave a comment