Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add lazy eval #92

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions lib/batch_loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
require_relative "./batch_loader/graphql"

class BatchLoader
IMPLEMENTED_INSTANCE_METHODS = %i[object_id __id__ __send__ singleton_method_added __sync respond_to? batch inspect].freeze
REPLACABLE_INSTANCE_METHODS = %i[batch inspect].freeze
IMPLEMENTED_INSTANCE_METHODS = %i[object_id __id__ __send__ singleton_method_added __sync respond_to? batch inspect lazy_eval __accepting_lazy_chain?].freeze
REPLACABLE_INSTANCE_METHODS = %i[batch inspect lazy_eval].freeze
LEFT_INSTANCE_METHODS = (IMPLEMENTED_INSTANCE_METHODS - REPLACABLE_INSTANCE_METHODS).freeze

NoBatchError = Class.new(StandardError)
Expand Down Expand Up @@ -37,6 +37,15 @@ def batch(default_value: nil, cache: true, replace_methods: nil, key: nil, &batc
self
end

def lazy_eval
@accepting_lazy_chain = true
@lazy_chain = []

__singleton_class.class_eval { undef_method(:lazy_eval) }

self
end

def respond_to?(method_name, include_private = false)
return true if LEFT_INSTANCE_METHODS.include?(method_name)

Expand All @@ -49,9 +58,13 @@ def inspect

def __sync
return @loaded_value if @synced
@accepting_lazy_chain = false

__ensure_batched
@loaded_value = __executor_proxy.loaded_value(item: @item)
(@lazy_chain || []).each do |method_name, args, kwargs, block|
@loaded_value = @loaded_value.public_send(method_name, *args, **kwargs, &block)
end

if @cache
@synced = true
Expand All @@ -62,6 +75,10 @@ def __sync
@loaded_value
end

def __accepting_lazy_chain?
@accepting_lazy_chain
end

private

def __loaded_value
Expand All @@ -70,7 +87,16 @@ def __loaded_value
end

def method_missing(method_name, *args, **kwargs, &block)
__sync!.public_send(method_name, *args, **kwargs, &block)
return __sync!.public_send(method_name, *args, **kwargs, &block) if !__accepting_lazy_chain?

return __sync! if method_name == :force

if method_name == :eager
@accepting_lazy_chain = false
else
@lazy_chain << [method_name, args, kwargs, block]
end
self
end

def __sync!
Expand Down
10 changes: 10 additions & 0 deletions lib/batch_loader/graphql.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,18 @@ def batch(**kwargs, &block)
self
end

def lazy_eval
@batch_loader.lazy_eval
self
end

def sync
@batch_loader.__sync
end

def method_missing(method_name, *args, **kwargs, &block)
super if !@batch_loader.__accepting_lazy_chain?
@batch_loader.public_send(method_name, *args, **kwargs, &block)
end
end
end
24 changes: 23 additions & 1 deletion spec/batch_loader_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@

expect(batch_loader.inspect).to match(/#<BatchLoader:0x\w+>/)
expect(batch_loader.to_s).to match(/#<User:0x\w+>/)
expect(batch_loader.inspect).to match(/#<User:0x\w+ @id=1>/)
expect(batch_loader.inspect).to match(/#<User:0x\w+ @id=1, @name=nil>/)
end
end

Expand Down Expand Up @@ -321,4 +321,26 @@
expect { result.to_s }.to raise_error("Oops")
end
end

describe "#lazy_eval" do
it 'allows modifying the end result without adding more loaders' do
user1 = User.save(id: 1, name: "John")
user2 = User.save(id: 2, name: "Jane")
post1 = Post.new(user_id: user1.id)
post2 = Post.new(user_id: user2.id)
result = { user1: post1.user_lazy.lazy_eval.name.eager, user2: post2.user_lazy.lazy_eval.id.eager }

expect(User).to receive(:where).with(id: [1, 2]).once.and_call_original

expect(result).to eq(user1: "John", user2: 2)
end

it 'delegates the second then call to the loaded value' do
user = User.save(id: 1)
post = Post.new(user_id: user.id)

user_lazy_instance = post.user_lazy.lazy_eval
expect(user_lazy_instance.lazy_eval.eager).to eq("Lazy Eval from User")
end
end
end
10 changes: 10 additions & 0 deletions spec/fixtures/graphql_schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ class UserType < GraphQL::Schema::Object

class PostType < GraphQL::Schema::Object
field :user, UserType, null: false
field :user_id, String, null: false
field :user_name, String, null: false
field :user_old, UserType, null: false

def user
Expand All @@ -12,6 +14,14 @@ def user
end
end

def user_id
user.lazy_eval.id
end

def user_name
user.lazy_eval.name
end

def user_old
BatchLoader.for(object.user_id).batch(default_value: nil) do |user_ids, loader|
User.where(id: user_ids).each { |user| loader.call(user.id, user) }
Expand Down
13 changes: 9 additions & 4 deletions spec/fixtures/models.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ def user_lazy(**opts)

class User
class << self
def save(id:)
def save(id:, name: nil)
ensure_init_store
@store[self][id] = new(id: id)
@store[self][id] = new(id: id, name: name)
end

def where(id:)
Expand All @@ -59,16 +59,21 @@ def ensure_init_store
end
end

attr_reader :id
attr_reader :id, :name

def initialize(id:)
def initialize(id:, name: nil)
@id = id
@name = name
end

def batch
"Batch from User"
end

def lazy_eval
"Lazy Eval from User"
end

def hash
[User, id].hash
end
Expand Down
10 changes: 6 additions & 4 deletions spec/graphql_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,16 @@
end

def test(schema)
user1 = User.save(id: "1")
user2 = User.save(id: "2")
user1 = User.save(id: "1", name: "John")
user2 = User.save(id: "2", name: "Jane")
Post.save(user_id: user1.id)
Post.save(user_id: user2.id)
query = <<~QUERY
{
posts {
user { id }
userName
userId
userOld { id }
}
}
Expand All @@ -36,8 +38,8 @@ def test(schema)

expect(result['data']).to eq({
'posts' => [
{'user' => {'id' => "1"}, 'userOld' => {'id' => "1"}},
{'user' => {'id' => "2"}, 'userOld' => {'id' => "2"}}
{'user' => {'id' => "1"}, 'userOld' => {'id' => "1"}, 'userId' => "1", 'userName' => "John"},
{'user' => {'id' => "2"}, 'userOld' => {'id' => "2"}, 'userId' => "2", 'userName' => "Jane"}
]
})
end
Expand Down
1 change: 1 addition & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require "bundler/setup"
require 'pry'

if ENV['CI']
require 'coveralls'
Expand Down