From 043a23e66199a383b5b0e91d39c9d68762ac0ec3 Mon Sep 17 00:00:00 2001 From: Vladimir Kovac Date: Sat, 30 Mar 2024 01:22:18 -0500 Subject: [PATCH] Add lazy eval --- lib/batch_loader.rb | 32 +++++++++++++++++++++++++++++--- lib/batch_loader/graphql.rb | 10 ++++++++++ spec/batch_loader_spec.rb | 24 +++++++++++++++++++++++- spec/fixtures/graphql_schema.rb | 10 ++++++++++ spec/fixtures/models.rb | 13 +++++++++---- spec/graphql_spec.rb | 10 ++++++---- spec/spec_helper.rb | 1 + 7 files changed, 88 insertions(+), 12 deletions(-) diff --git a/lib/batch_loader.rb b/lib/batch_loader.rb index d5ccdd2..cf998b6 100644 --- a/lib/batch_loader.rb +++ b/lib/batch_loader.rb @@ -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) @@ -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) @@ -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 @@ -62,6 +75,10 @@ def __sync @loaded_value end + def __accepting_lazy_chain? + @accepting_lazy_chain + end + private def __loaded_value @@ -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! diff --git a/lib/batch_loader/graphql.rb b/lib/batch_loader/graphql.rb index f8ae756..9f75188 100644 --- a/lib/batch_loader/graphql.rb +++ b/lib/batch_loader/graphql.rb @@ -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 diff --git a/spec/batch_loader_spec.rb b/spec/batch_loader_spec.rb index f5b6678..8369b01 100644 --- a/spec/batch_loader_spec.rb +++ b/spec/batch_loader_spec.rb @@ -220,7 +220,7 @@ expect(batch_loader.inspect).to match(/#/) expect(batch_loader.to_s).to match(/#/) - expect(batch_loader.inspect).to match(/#/) + expect(batch_loader.inspect).to match(/#/) end end @@ -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 diff --git a/spec/fixtures/graphql_schema.rb b/spec/fixtures/graphql_schema.rb index 09e711c..7c393a3 100644 --- a/spec/fixtures/graphql_schema.rb +++ b/spec/fixtures/graphql_schema.rb @@ -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 @@ -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) } diff --git a/spec/fixtures/models.rb b/spec/fixtures/models.rb index 68466a3..326738b 100644 --- a/spec/fixtures/models.rb +++ b/spec/fixtures/models.rb @@ -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:) @@ -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 diff --git a/spec/graphql_spec.rb b/spec/graphql_spec.rb index 496dfa0..942cfc5 100644 --- a/spec/graphql_spec.rb +++ b/spec/graphql_spec.rb @@ -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 } } } @@ -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 diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 68b0934..d403a0f 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,4 +1,5 @@ require "bundler/setup" +require 'pry' if ENV['CI'] require 'coveralls'