How to refactor queries chain in Hanami? - ruby

How to refactor #filtered method?
In Hanami there is no way to make a chain of queries (filters) in ActiveRecord-style. I would like to get a methods like ActiveRecord filters.
Now: documents.filtered(genre: 'news', min_published_at: from, max_published_at: to, skip: 30)
What I want: documents.with_genre('news').published_between(from, to).skip(30)
class DocumentRepository < Hanami::Repository
GENRES = DbSchema.current_schema.enum(:document_genre).values.map(&:to_s)
DOCUMENTS_PER_PAGE = 30
associations do
has_many :boxes
has_many :urls
end
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/AbcSize
def filtered(params = {})
result = ordered.limit(DOCUMENTS_PER_PAGE)
result = result.where(genre: params[:genre]) if params.key?(:genre)
if params.key?(:min_created_at) && params.key?(:max_created_at)
date_range = params[:min_created_at]..params[:max_created_at]
result = result.where(created_at: date_range)
end
if params.key?(:min_published_at) && params.key?(:max_published_at)
date_range = params[:min_published_at]..params[:max_published_at]
result = result.where(published_at: date_range)
end
result = result.offset(params[:skip]) if params.key?(:skip)
result
end
# rubocop:enable Metrics/CyclomaticComplexity
# rubocop:enable Metrics/AbcSize
def ordered
documents.order { created_at.desc }
end
end

Something along these lines might work, but not sure how chaining these will poorly effect performance or results, but you can try it and it may lead you to the answer you want
UPDATED
If you really want chaining this is close to what you want.
class DocumentRepository < Hanami::Repository
GENRES = DbSchema.current_schema.enum(:document_genre).values.map(&:to_s)
DOCUMENTS_PER_PAGE = 30
associations do
has_many :boxes
has_many :urls
end
attr_accessor :data
def initialize
#data = []
super
end
def data
#data.flatten!.uniq!
end
def with_genre(key)
#data << documents.where(genre: key)
self
end
def published_between(arr)
from, to = arr
#data << documents.where(created_at: [from..to])
self
end
def skip(num)
#data << documents.offset(num)
self
end
end
Call it like this assuming this is an instance variable of DocumentRepository
document_repository.with_genre('news')
.published_between([from, to])
.skip(30)
.data
By returning self in each instance method you're able to chain the calls on the instance.
Original answer
This way works but uses similar syntax in your current call.
class DocumentRepository < Hanami::Repository
GENRES = DbSchema.current_schema.enum(:document_genre).values.map(&:to_s)
DOCUMENTS_PER_PAGE = 30
associations do
has_many :boxes
has_many :urls
end
def hack_where(opts={})
data = []
opts.each do |i|
data << self.send(i[0],i[1]).call
end
data.flatten!.uniq!
end
def with_genre(key)
lambda { |key| documents.where(genre: key) }
end
def published_between(arr)
from = arr[0]
to = arr[1]
lambda { |from, to| documents.where(created_at: [from..to]) }
end
def skip(num)
lambda { documents.offset(num) }
end
end
You can call it like:
hack_where({with_genre: 'news', published_between: [from,to], skip: 30})

Introduce query object:
class FilterDocuments
DOCUMENTS_PER_PAGE = 30
def initialize(documents)
#documents = documents
end
def filter(params = {})
result = apply_ordering(documents)
result = apply_limit_and_offset(result, params)
result = filter_by_genre(result, params)
result = filter_by_created_at(result, params)
result = filter_by_published_at(result, params)
result
end
private
attr_reader :documents
def apply_ordering(documents)
documents.order { created_at.desc }
end
def apply_limit_and_offset(documents, params)
if params.key?(:skip)
documents.offset(params[:skip])
else
documents
end.limit(DOCUMENTS_PER_PAGE)
end
def filter_by_genre(documents, params)
if params.key?(:genre)
documents.where(genre: params[:genre])
else
documents
end
end
def filter_by_created_at(documents, params)
if params.key?(:min_created_at) && params.key?(:max_created_at)
range = params[:min_created_at]..params[:max_created_at]
documents.where(created_at: range)
else
documents
end
end
def filter_by_published_at(documents, params)
if params.key?(:min_published_at) && params.key?(:max_published_at)
range = params[:min_published_at]..params[:max_published_at]
documents.where(published_at: range)
else
documents
end
end
end
How to use:
def query
FilterDocuments.new(DocumentRepository.new.documents)
end
filtered_documents = query.filter(params)

Related

I have an array of objects with parameters(in hash). How can I list all the parameters of every object?

Item class
class Item
def initialize(options = {})
#name = options[:name]
#code = options[:code]
#category = options[:category]
#size = options[:size]
end
attr_accessor :name, :code, :category, :size
end
Music class
class Music < Item
def initialize(options = {})
super
#singer = options[:singer]
#duration = options[:duration]
end
attr_accessor :singer, :duration
end
Movie class
def initialize(options = {})
super
#director = options[:director]
#main_actor = options[:main_actor]
#main_actress = options[:main_actress]
end
attr_accessor :director, :main_actor, :main_actress
end
class Catalog
attr_reader :items_list
def initialize
#items_list = Array.new
end
def add(item)
#items_list.push item
end
def remove(code)
#items_list.delete_if { |i| i.code == code }
end
def show(code)
# comming soon
end
def list
#items_list.each do |array|
array.each { |key, value| puts "#{key} => #{value}" }
end
end
end
catalog1 = Catalog.new
music1 = Music.new(name: "Venom", code: 1, category: :music, size: 1234, singer: "Some singer", duration: 195)
music2 = Music.new(name: "Champion of Death", code: 2, category: :music, size: 1234, singer: "Some singer", duration: 195)
catalog1.add(music1)
catalog1.add(music2)
ruby version 2.6.0
list method is not working. I got undefined method `each' for <#Music:0x0000562e8ebe9d18>.
How can I list all keys and values in another way? Like:
name - "Venom"
code - 1
category - music.
I was thinking about it, but also I got a Movie class and that method gonna be too long
You push instances of Music into #items_list. That means #items_list.each do not return an array, but instances of Music and that Musik instances do not respond do each nor they have keys and values.
I suggest adding an instance method to your Music class that returns the expected output. For example a to_s method like this:
def to_s
"name \"#{name}\" code - #{code} category - #{category}"
end
and to change the list method in your Catalog to something like this:
def list
#items_list.each do |music|
puts music.to_s
end
end
Or when you want to return the values an array of hashed then add a to_h method to Music like this:
def to_h
{ name: name, code: code, category: category }
end
and call it like this:
def list
#items_list.map do |music|
music.to_h
end
end

I need to override pundit scope but I don't know how to

I have a question about Pundit.
Basically I want to do this:
class Scope < Scope
def resolve
scope.select {|employee| (employee.restaurant == #restaurant) && employee.actif}
end
end
but I don't know how I can pass #restaurant from my controller into my policy.
here's my index method:
def index
#restaurant = Restaurant.find(params[:restaurant_id])
#employees = policy_scope(Employee)
end
I tried to do this:
class Scope
attr_reader :user, :scope, :record
def initialize(user, scope, record)
#user = user
#scope = scope
#record = record
end
def resolve
if is_user_manager_or_gerant_or_admin?
scope.select {|employee| (employee.restaurant == record) && employee.actif}
end
end
private
def is_user_manager_or_gerant_or_admin?
user.admin || (user.accreditations.select {|a| a.restaurant == record})[0].role == ("GĂ©rant" || "Manager")
end
end
with this index method:
def index
#restaurant = Restaurant.find(params[:restaurant_id])
#employees = EmployeePolicy::Scope.new(current_user, Employee, #restaurant).resolve
end
But I'm getting this error:
Pundit::PolicyScopingNotPerformedError in EmployeesController#index
It's not clear from your question where you put the Scope class, but it should live inside your EmployeePolicy class. You'll pass in a parameter just like you would to any other class.
I think you are getting this error because you are inadvertently converting your ActiveRecord scope to an Array by using the #select method. Try using ActiveRecord methods instead.
# app/policies/employee_policy.rb
class EmployeePolicy < ApplicationPolicy
class Scope
attr_reader :user, :scope, :restaurant
def initialize(user, scope, restaurant)
#user = user
#scope = scope
#restaurant = restaurant
end
def resolve
if is_user_manager_or_gerant_or_admin?
scope.where(restaurant: restaurant).where.not(employee.actif: nil)
end
end
...
end
end
Your index method should look like this:
# app/controllers/employees_controller.rb
class EmployeesController < ApplicationController
def index
#restaurant = Restaurant.find(params[:restaurant_id])
#employees = EmployeePolicy::Scope.new(current_user, Employee, #restaurant).resolve
end
end
If you are still having trouble, then this post may be instructive.

Modeling a cookie with object composition in Ruby

I'm a new Rubyist and am wondering how I can access the ingredients class from individual cookies? As we all know, cookies are made of different ingredients. How can I specify default ingredients for individual cookies without setting default values? Even if I had default values, how would I update those to reflect the most current "recipe"? Please and thanks!
#Cookie Factory
module CookieFactory
def self.create(args)
cookie_batch = []
args.each do |cookie|
cookie_batch << PeanutButter.new if cookie == "peanut butter"
cookie_batch << ChocholateChip.new if cookie == "chocolate chip"
cookie_batch << Sugar.new if cookie == "sugar"
end
return cookie_batch
end
end
#Classes/Subclasses
class Ingredients
attr_reader
def initialize(contents = {})
# contents = defaults.merge(contents)
#sugar = contents.fetch(:sugar, "1.5 cups")
#salt = contents.fetch(:salt, "1 teaspoon")
#gluten = contents.fetch(:gluten, "0")
#cinnamon = contents.fetch(:cinnamon, "0.5 teaspoon")
end
end
class Cookie
attr_reader :status, :ingredients
def initialize(ingredients = {})
#ingredients = ingredients
#status = :doughy
super()
end
def bake!
#status = :baked
end
end
class PeanutButter < Cookie
attr_reader :peanut_count
def initialize
#peanut_count = 100
super()
end
def defaults
{
:peanut_shells => 5
}
end
end
class Sugar < Cookie
attr_reader :sugar
def initialize
#sugar = "1_cup"
super()
end
end
class ChocholateChip < Cookie
attr_reader :choc_chip_count
def initialize
#choc_chip_count = 200
super()
end
end
You can use Hash#merge to acheive this behavior:
class PeanutButter < Cookie
attr_reader :peanut_count
def initialize(ingredients)
#peanut_count = 100
super(ingredients.merge(defaults))
end
def defaults
{
:peanut_shells => 5
}
end
end

Ruby - "undefined method 'at'"

class Book
def isBook()
return true
end
def initialize(isbn, userID)
#isbn = isbn
#userID = userID
end
def ==(var)
if(var.isbn == #isbn && var.userID == #userID)
return true
end
return false
end
def print()
"ISBN: %{#isbn}\nWypozyczono przez: %{#userID}"
end
end
class BookList
def initialize()
#arr = Array.new()
end
def add(book)
if(book.isBook())
#arr.push(book)
end
end
def at(var)
#arr[var].print()
end
end
booklist = BookList.new()
print booklist
booklist.add(Book.new(1231231231231, "d_zeglen"))
print BookList.at(0)
I don't know where did I made mistake. When I try to run this program, ruby prints into console:
undefined method 'at' for BookList:Class (No method error)
Anybody got idea what's wrong here?
#at is an instance method of the instances of the class BookList, not a class method. Thus below will work
print booklist.at(0)
Here is the code :-
class Book
def isBook()
return true
end
def initialize(isbn, userID)
#isbn = isbn
#userID = userID
end
def ==(var)
if(var.isbn == #isbn && var.userID == #userID)
return true
end
return false
end
def print()
"ISBN: %{#isbn}\nWypozyczono przez: %{#userID}"
end
end
class BookList
def initialize()
#arr = Array.new()
end
def add(book)
if(book.isBook())
#arr.push(book)
end
end
def at(var)
#arr[var].print()
end
end
booklist = BookList.new()
print booklist
booklist.add(Book.new(1231231231231, "d_zeglen"))
print booklist.at(0)
# >> #<BookList:0xa013de4>ISBN: %{#isbn}
# >> Wypozyczono przez: %{#userID}

Ruby object returned from method loses class type (reverts to base class array)

In the following code, the issue is that after calling method .find_name on an object type of LogsCollection, the returned object becomes a native array and does not remain type LogsCollection. I believe the correct approach might be to create a constructor/initializer that accepts an array and return a brand new object of the correct type. But I am not sure there is not a better way to accomplish this?
Can a Ruby-pro eyeball this code and suggest (at the code level) the best way to make the returned object from .find_name remain type LogsCollection (not array)?
class Log
attr_accessor :name, :expense_id, :is_excluded, :amount, :paid_to
def initialize(name, expense_id, is_excluded, amount, paid_to)
#name = name
#expense_id = expense_id
#is_excluded = is_excluded
#amount = amount
#paid_to = paid_to
end
end
class LogsCollection < Array
def names
collect do |i|
i.name
end
end
def find_name(name)
#name = name
self.select { |l| l.name == #name }
end
end
logs = LogsCollection.new
logs.push(Log.new('Smith', 1, false, 323.95, nil))
logs.push(Log.new('Jones', 1, false, 1000, nil))
logs = logs.find_name('Smith')
puts logs.count
unless logs.empty?
puts logs.first.name # works since this is a standard function in native array
puts logs.names # TODO: figure out why this fails (we lost custom class methods--LogsCollection def find_name returns _native_ array, not type LogsCollection)
end
Final code post-answer for anyone searching (note the removal of base class < array):
class Log
attr_accessor :name, :expense_id, :is_excluded, :amount, :paid_to
def initialize(name, expense_id, is_excluded, amount, paid_to)
#name = name
#expense_id = expense_id
#is_excluded = is_excluded
#amount = amount
#paid_to = paid_to
end
end
class LogsCollection
attr_reader :logs
def initialize(logs)
#logs = logs
end
def add(log)
#logs.push(log)
end
def names
#logs.collect { |l| l.name }
end
def find_name(name)
LogsCollection.new(#logs.select { |l| l.name == name })
end
end
logs = LogsCollection.new([])
logs.add(Log.new('Smith', 1, false, 323.95, nil))
logs.add(Log.new('Jones', 1, false, 1000, nil))
puts logs.names
puts '--- post .find_name ---'
puts logs.find_name('Smith').names
As you can see in the docs Enumerable#select with a block always returns an array. E.g.
{:a => 1, :b => 2, :c => 3}.select { |k,v | v > 1 }
=> [[:b, 2], [:c, 3]]
What you could do is have some sort of constructor for LogsCollection that wraps up a normal array as a LogsCollection object and call that in find_name.
As requested here's an example class (I'm at work and writing this while waiting for something to finish, it's completely untested):
class LogsCollection
attr_reader :logs
def initialize(logs)
#logs = logs
end
def names
#logs.collect { |i| i.name }
end
def find_name(n)
name = n
LogsCollection.new(#logs.select { |l| l.name == n })
end
# if we don't know a method, forward it to the #logs array
def method_missing(m, *args, &block)
#logs.send(m, args, block)
end
end
Use like
lc = LogsCollection.new
logs = lc.logs.find_name('Smith')

Resources