From e53a828ae3bb0d0709f844d07d519ff2040817b7 Mon Sep 17 00:00:00 2001 From: Alessandro Desantis Date: Wed, 1 Apr 2020 15:15:36 +0200 Subject: [PATCH 1/9] Rename Renderful::Cache to Renderful::Cache::Base --- lib/renderful.rb | 2 +- lib/renderful/cache.rb | 21 --------------------- lib/renderful/cache/base.rb | 23 +++++++++++++++++++++++ lib/renderful/cache/redis.rb | 4 ++-- 4 files changed, 26 insertions(+), 24 deletions(-) delete mode 100644 lib/renderful/cache.rb create mode 100644 lib/renderful/cache/base.rb diff --git a/lib/renderful.rb b/lib/renderful.rb index 485e044..d5c95d0 100644 --- a/lib/renderful.rb +++ b/lib/renderful.rb @@ -3,7 +3,7 @@ require 'contentful' require 'renderful/no_renderer_error' -require 'renderful/cache' +require 'renderful/cache/base' require 'renderful/cache/redis' require 'renderful/cache_invalidator' require 'renderful/client' diff --git a/lib/renderful/cache.rb b/lib/renderful/cache.rb deleted file mode 100644 index 2dc7cac..0000000 --- a/lib/renderful/cache.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -module Renderful - class Cache - def exist?(_key) - raise NotImplementedError - end - - def read(_key) - raise NotImplementedError - end - - def write(_key, _value) - raise NotImplementedError - end - - def delete(_key) - raise NotImplementedError - end - end -end diff --git a/lib/renderful/cache/base.rb b/lib/renderful/cache/base.rb new file mode 100644 index 0000000..7c1cd80 --- /dev/null +++ b/lib/renderful/cache/base.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Renderful + module Cache + class Base + def exist?(_key) + raise NotImplementedError + end + + def read(_key) + raise NotImplementedError + end + + def write(_key, _value) + raise NotImplementedError + end + + def delete(_key) + raise NotImplementedError + end + end + end +end diff --git a/lib/renderful/cache/redis.rb b/lib/renderful/cache/redis.rb index 1fb03c4..5bffc63 100644 --- a/lib/renderful/cache/redis.rb +++ b/lib/renderful/cache/redis.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true module Renderful - class Cache - class Redis < Cache + module Cache + class Redis < Base attr_reader :redis def initialize(redis) From 0888ed3316acc63a7ebffc8bf1a0df637150901f Mon Sep 17 00:00:00 2001 From: Alessandro Desantis Date: Wed, 1 Apr 2020 15:20:36 +0200 Subject: [PATCH 2/9] Rename renderers to components --- README.md | 40 +++++++++---------- lib/renderful.rb | 6 +-- lib/renderful/client.rb | 12 +++--- lib/renderful/component/base.rb | 38 ++++++++++++++++++ .../{renderer => component}/rails.rb | 6 +-- ...enderer_error.rb => no_component_error.rb} | 4 +- lib/renderful/renderer.rb | 36 ----------------- ...test_component.html.erb => _test.html.erb} | 0 spec/renderful/cache_invalidator_spec.rb | 2 +- spec/renderful/client_spec.rb | 26 ++++++------ .../{renderer => component}/rails_spec.rb | 12 +++--- 11 files changed, 92 insertions(+), 90 deletions(-) create mode 100644 lib/renderful/component/base.rb rename lib/renderful/{renderer => component}/rails.rb (80%) rename lib/renderful/{no_renderer_error.rb => no_component_error.rb} (53%) delete mode 100644 lib/renderful/renderer.rb rename spec/internal/app/views/renderful/{_test_component.html.erb => _test.html.erb} (100%) rename spec/renderful/{renderer => component}/rails_spec.rb (64%) diff --git a/README.md b/README.md index 3c42b37..1384bc7 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,8 @@ contentful = Contentful::Client.new( renderful = Renderful.new( contentful: contentful, - renderers: { - 'jumbotron' => JumbotronRenderer, + components: { + 'jumbotron' => JumbotronComponent, } ) ``` @@ -46,10 +46,10 @@ renderful = Renderful.new( Suppose you have the `jumbotron` content type in your Contentful space. This content type has the `title` and `content` fields, both strings. -Let's create the `app/renderers/jumbotron_renderer.rb` file: +Let's create the `app/components/jumbotron_component.rb` file: ```ruby -class JumbotronRenderer < Renderful::Renderer +class JumbotronComponent < Renderful::Component def render <<~HTML
@@ -74,7 +74,7 @@ If you have rich-text fields, you can leverage Contentful's [rich_text_renderer] along with a custom local variable: ```ruby -class TextBlockRenderer < Renderful::Renderer::Rails +class TextBlockComponent < Renderful::Component::Rails def html_body RichTextRenderer::Renderer.new.render(entry.body) end @@ -100,7 +100,7 @@ all of the content entries contained in that field: ```ruby # app/components/grid.rb -class Grid < Renderful::Renderer +class Grid < Renderful::Component # This will define a `resolved_blocks` method that reads external references # from the `blocks` fields and turns them into Contentful::Entry instances resolve :blocks @@ -124,7 +124,7 @@ end ### Caching -You can easily cache the output of your renderers by passing a `cache` key when instantiating the +You can easily cache the output of your components by passing a `cache` key when instantiating the client. The value of this key should be an object that responds to the following methods: - `#read(key)` @@ -138,8 +138,8 @@ A Redis cache implementation is included out of the box. Here's an example: renderful = Renderful.new( contentful: contentful, cache: Renderful::Cache::Redis.new(Redis.new(url: 'redis://localhost:6379')), - renderers: { - 'jumbotron' => JumbotronRenderer + components: { + 'jumbotron' => JumbotronComponent } ) ``` @@ -151,8 +151,8 @@ If you are using Rails and want to use the Rails cache store for Renderful, you renderful = Renderful.new( contentful: contentful, cache: Rails.cache, - renderers: { - 'jumbotron' => JumbotronRenderer + components: { + 'jumbotron' => JumbotronComponent } ) ``` @@ -188,11 +188,11 @@ components is updated, you want the page to be re-rendered. ### Rails integration -If you are using Ruby on Rails and you want to use ERB instead of including HTML in your renderers, -you can inherit from the Rails renderer: +If you are using Ruby on Rails and you want to use ERB instead of including HTML in your components, +you can inherit from the Rails component: ```ruby -class JumbotronRenderer < Renderful::Renderer::Rails +class JumbotronComponent < Renderful::Component::Rails end ``` @@ -209,12 +209,12 @@ As you can see, you can access the Contentful entry via the `entry` local variab #### Custom renderer -The Rails renderer uses `ActionController::Base.renderer` by default, but this prevents you from +Rails components use `ActionController::Base.renderer` by default, but this prevents you from using your own helpers in components. If you want to use a different renderer instead, you can override the `renderer` method: ```ruby -class JumbotronRenderer < Renderful::Renderer::Rails +class JumbotronComponent < Renderful::Component::Rails def renderer ApplicationController.renderer end @@ -226,7 +226,7 @@ end If you want, you can also add your own locals: ```ruby -class JumbotronRenderer < Renderful::Renderer::Rails +class JumbotronComponent < Renderful::Component::Rails def locals italian_title = entry.title.gsub(/hello/, 'ciao') { italian_title: italian_title } @@ -248,13 +248,13 @@ You would then access them like regular locals: #### Resolution in ERB views -If you need to render resolved fields (as in our `Grid` example), you can use `renderer` and -`client` to access the `Renderful::Renderer` and `Renderful::Client` objects: +If you need to render resolved fields (as in our `Grid` example), you can use `component` and +`client` to access the `Renderful::Component` and `Renderful::Client` objects: ```erb <%# app/views/renderful/_grid.html.erb %>
- <% renderer.blocks.each do |block| %> + <% component.blocks.each do |block| %>
<%= client.render(block) %>
diff --git a/lib/renderful.rb b/lib/renderful.rb index d5c95d0..2d23443 100644 --- a/lib/renderful.rb +++ b/lib/renderful.rb @@ -2,13 +2,13 @@ require 'contentful' -require 'renderful/no_renderer_error' +require 'renderful/no_component_error' require 'renderful/cache/base' require 'renderful/cache/redis' require 'renderful/cache_invalidator' require 'renderful/client' -require 'renderful/renderer' -require 'renderful/renderer/rails' +require 'renderful/component/base' +require 'renderful/component/rails' require 'renderful/version' module Renderful diff --git a/lib/renderful/client.rb b/lib/renderful/client.rb index cfd771a..e29fd09 100644 --- a/lib/renderful/client.rb +++ b/lib/renderful/client.rb @@ -2,21 +2,21 @@ module Renderful class Client - attr_reader :contentful, :renderers, :cache + attr_reader :contentful, :components, :cache - def initialize(contentful:, renderers:, cache: nil) + def initialize(contentful:, components:, cache: nil) @contentful = contentful - @renderers = renderers + @components = components @cache = cache end def render(entry) - renderer = renderers[entry.content_type.id] - fail(NoRendererError, entry) unless renderer + component = components[entry.content_type.id] + fail(NoComponentError, entry) unless component return cache.read(cache_key_for(entry)) if cache&.exist?(cache_key_for(entry)) - renderer.new(entry, client: self).render.tap do |output| + component.new(entry, client: self).render.tap do |output| cache&.write(cache_key_for(entry), output) end end diff --git a/lib/renderful/component/base.rb b/lib/renderful/component/base.rb new file mode 100644 index 0000000..96ed173 --- /dev/null +++ b/lib/renderful/component/base.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Renderful + module Component + class Base + attr_reader :entry, :client + + class << self + def resolve(field) + define_method(field) do + resolve(entry.send(field)) + end + end + end + + def initialize(entry, client:) + @entry = entry + @client = client + end + + def render + raise NotImplementedError + end + + private + + def resolve(reference) + if reference.is_a?(Enumerable) + reference.map(&method(:resolve)) + elsif reference.is_a?(Contentful::Link) + reference.resolve(client.contentful) + else + reference + end + end + end + end +end diff --git a/lib/renderful/renderer/rails.rb b/lib/renderful/component/rails.rb similarity index 80% rename from lib/renderful/renderer/rails.rb rename to lib/renderful/component/rails.rb index 37bd08e..04d6c06 100644 --- a/lib/renderful/renderer/rails.rb +++ b/lib/renderful/component/rails.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true module Renderful - class Renderer - class Rails < Renderer + module Component + class Rails < Base def render renderer.render(partial: view, locals: locals.merge(default_locals)) end @@ -22,7 +22,7 @@ def view end def default_locals - { entry: entry, client: client, renderer: self } + { entry: entry, client: client, component: self } end end end diff --git a/lib/renderful/no_renderer_error.rb b/lib/renderful/no_component_error.rb similarity index 53% rename from lib/renderful/no_renderer_error.rb rename to lib/renderful/no_component_error.rb index 7fbe5e0..baaacb9 100644 --- a/lib/renderful/no_renderer_error.rb +++ b/lib/renderful/no_component_error.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true module Renderful - class NoRendererError < StandardError + class NoComponentError < StandardError attr_reader :entry def initialize(entry, *args) @entry = entry - super "Cannot find renderer for content type #{entry.content_type.id}", *args + super "Cannot find component for content type #{entry.content_type.id}", *args end end end diff --git a/lib/renderful/renderer.rb b/lib/renderful/renderer.rb deleted file mode 100644 index c27d07b..0000000 --- a/lib/renderful/renderer.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -module Renderful - class Renderer - attr_reader :entry, :client - - class << self - def resolve(field) - define_method(field) do - resolve(entry.send(field)) - end - end - end - - def initialize(entry, client:) - @entry = entry - @client = client - end - - def render - raise NotImplementedError - end - - private - - def resolve(reference) - if reference.is_a?(Enumerable) - reference.map(&method(:resolve)) - elsif reference.is_a?(Contentful::Link) - reference.resolve(client.contentful) - else - reference - end - end - end -end diff --git a/spec/internal/app/views/renderful/_test_component.html.erb b/spec/internal/app/views/renderful/_test.html.erb similarity index 100% rename from spec/internal/app/views/renderful/_test_component.html.erb rename to spec/internal/app/views/renderful/_test.html.erb diff --git a/spec/renderful/cache_invalidator_spec.rb b/spec/renderful/cache_invalidator_spec.rb index e3ea9ca..eaaabc0 100644 --- a/spec/renderful/cache_invalidator_spec.rb +++ b/spec/renderful/cache_invalidator_spec.rb @@ -64,7 +64,7 @@ end context 'when caching is enabled on the client' do - let(:cache) { instance_spy('Renderful::Cache') } + let(:cache) { instance_spy('Renderful::Cache::Base') } before do allow(client).to receive(:cache_key_for) diff --git a/spec/renderful/client_spec.rb b/spec/renderful/client_spec.rb index d632b7b..28c439a 100644 --- a/spec/renderful/client_spec.rb +++ b/spec/renderful/client_spec.rb @@ -4,19 +4,19 @@ RSpec.describe Renderful::Client do subject(:client) do - described_class.new(contentful: contentful, renderers: renderers, cache: cache) + described_class.new(contentful: contentful, components: components, cache: cache) end let(:contentful) { instance_double('Contentful::Client') } - let(:renderers) do + let(:components) do { - 'testContentType' => renderer_klass, + 'testContentType' => component_klass, } end - let(:cache) { instance_spy('Renderful::Cache') } + let(:cache) { instance_spy('Renderful::Cache::Base') } - let(:renderer_klass) { class_double('Renderful::Renderer') } + let(:component_klass) { class_double('Renderful::Component::Base') } describe '#cache_key_for' do context 'with an entry' do @@ -43,9 +43,9 @@ describe '#render' do let(:entry) { OpenStruct.new(content_type: OpenStruct.new(id: content_type_id)) } - let(:cache) { instance_spy('Renderful::Cache') } + let(:cache) { instance_spy('Renderful::Cache::Base') } - context 'when a renderer has been registered for the provided content type' do + context 'when a component has been registered for the provided content type' do let(:content_type_id) { 'testContentType' } context 'when the output has been cached' do @@ -72,12 +72,12 @@ .with(an_instance_of(String)) .and_return(false) - allow(renderer_klass).to receive(:new) + allow(component_klass).to receive(:new) .with(entry, client: client) - .and_return(instance_double('Renderful::Renderer', render: 'render_output')) + .and_return(instance_double('Renderful::Component::Base', render: 'render_output')) end - it 'renders the content type with its renderer' do + it 'renders the content type with its component' do result = client.render(entry) expect(result).to eq('render_output') @@ -91,13 +91,13 @@ end end - context 'when no renderer has been registered for the provided content type' do + context 'when no component has been registered for the provided content type' do let(:content_type_id) { 'unknownContentType' } - it 'raises a NoRendererError' do + it 'raises a NoComponentError' do expect { client.render(entry) - }.to raise_error(Renderful::NoRendererError) + }.to raise_error(Renderful::NoComponentError) end end end diff --git a/spec/renderful/renderer/rails_spec.rb b/spec/renderful/component/rails_spec.rb similarity index 64% rename from spec/renderful/renderer/rails_spec.rb rename to spec/renderful/component/rails_spec.rb index 5400ea4..23838f6 100644 --- a/spec/renderful/renderer/rails_spec.rb +++ b/spec/renderful/component/rails_spec.rb @@ -2,16 +2,16 @@ require 'spec_helper' -RSpec.describe Renderful::Renderer::Rails do - subject(:renderer) do - TestComponentRenderer.new(entry, client: client) +RSpec.describe Renderful::Component::Rails do + subject(:component) do + TestComponent.new(entry, client: client) end - let(:entry) { OpenStruct.new(content_type: OpenStruct.new(id: 'testComponent')) } + let(:entry) { OpenStruct.new(content_type: OpenStruct.new(id: 'test')) } let(:client) { instance_double('Renderful::Client') } before(:all) do - TestComponentRenderer = Class.new(described_class) do + TestComponent = Class.new(described_class) do def locals { test_local: 'local_value' } end @@ -20,7 +20,7 @@ def locals describe '#render' do it 'renders the correct partial' do - result = renderer.render + result = component.render expect(result.strip).to eq('test_local is local_value') end From a64e809c423dd93296332b21e15b09fa88d6066b Mon Sep 17 00:00:00 2001 From: Alessandro Desantis Date: Wed, 1 Apr 2020 15:22:27 +0200 Subject: [PATCH 3/9] Move errors to a separate namespace --- lib/renderful.rb | 3 ++- lib/renderful/client.rb | 2 +- lib/renderful/error/base.rb | 5 +++++ lib/renderful/error/no_component_error.rb | 15 +++++++++++++++ lib/renderful/no_component_error.rb | 13 ------------- lib/renderful/provider/base.rb | 0 spec/renderful/client_spec.rb | 2 +- 7 files changed, 24 insertions(+), 16 deletions(-) create mode 100644 lib/renderful/error/base.rb create mode 100644 lib/renderful/error/no_component_error.rb delete mode 100644 lib/renderful/no_component_error.rb create mode 100644 lib/renderful/provider/base.rb diff --git a/lib/renderful.rb b/lib/renderful.rb index 2d23443..8a4f7d3 100644 --- a/lib/renderful.rb +++ b/lib/renderful.rb @@ -2,7 +2,8 @@ require 'contentful' -require 'renderful/no_component_error' +require 'renderful/error/base' +require 'renderful/error/no_component_error' require 'renderful/cache/base' require 'renderful/cache/redis' require 'renderful/cache_invalidator' diff --git a/lib/renderful/client.rb b/lib/renderful/client.rb index e29fd09..a2e5ec2 100644 --- a/lib/renderful/client.rb +++ b/lib/renderful/client.rb @@ -12,7 +12,7 @@ def initialize(contentful:, components:, cache: nil) def render(entry) component = components[entry.content_type.id] - fail(NoComponentError, entry) unless component + fail(Error::NoComponentError, entry) unless component return cache.read(cache_key_for(entry)) if cache&.exist?(cache_key_for(entry)) diff --git a/lib/renderful/error/base.rb b/lib/renderful/error/base.rb new file mode 100644 index 0000000..2943da6 --- /dev/null +++ b/lib/renderful/error/base.rb @@ -0,0 +1,5 @@ +module Renderful + module Error + class Base < StandardError; end + end +end diff --git a/lib/renderful/error/no_component_error.rb b/lib/renderful/error/no_component_error.rb new file mode 100644 index 0000000..b5167cd --- /dev/null +++ b/lib/renderful/error/no_component_error.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Renderful + module Error + class NoComponentError < Base + attr_reader :entry + + def initialize(entry, *args) + @entry = entry + + super "Cannot find component for content type #{entry.content_type.id}", *args + end + end + end +end diff --git a/lib/renderful/no_component_error.rb b/lib/renderful/no_component_error.rb deleted file mode 100644 index baaacb9..0000000 --- a/lib/renderful/no_component_error.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -module Renderful - class NoComponentError < StandardError - attr_reader :entry - - def initialize(entry, *args) - @entry = entry - - super "Cannot find component for content type #{entry.content_type.id}", *args - end - end -end diff --git a/lib/renderful/provider/base.rb b/lib/renderful/provider/base.rb new file mode 100644 index 0000000..e69de29 diff --git a/spec/renderful/client_spec.rb b/spec/renderful/client_spec.rb index 28c439a..13957d0 100644 --- a/spec/renderful/client_spec.rb +++ b/spec/renderful/client_spec.rb @@ -97,7 +97,7 @@ it 'raises a NoComponentError' do expect { client.render(entry) - }.to raise_error(Renderful::NoComponentError) + }.to raise_error(Renderful::Error::NoComponentError) end end end From 285726a2f4815624be222949e05f432ea4a9feb7 Mon Sep 17 00:00:00 2001 From: Alessandro Desantis Date: Wed, 1 Apr 2020 16:11:18 +0200 Subject: [PATCH 4/9] Create providers-based architecture --- lib/renderful.rb | 5 +- lib/renderful/cache/base.rb | 4 + lib/renderful/cache/null.rb | 27 +++++ lib/renderful/cache/redis.rb | 8 ++ lib/renderful/cache_invalidator.rb | 26 ----- lib/renderful/client.rb | 39 ++++---- lib/renderful/content_entry.rb | 25 +++++ lib/renderful/error/no_component_error.rb | 2 +- lib/renderful/provider/base.rb | 23 +++++ lib/renderful/provider/contentful.rb | 52 ++++++++++ spec/renderful/cache_invalidator_spec.rb | 110 --------------------- spec/renderful/client_spec.rb | 115 +++++++++------------- 12 files changed, 213 insertions(+), 223 deletions(-) create mode 100644 lib/renderful/cache/null.rb delete mode 100644 lib/renderful/cache_invalidator.rb create mode 100644 lib/renderful/content_entry.rb create mode 100644 lib/renderful/provider/contentful.rb delete mode 100644 spec/renderful/cache_invalidator_spec.rb diff --git a/lib/renderful.rb b/lib/renderful.rb index 8a4f7d3..e877e36 100644 --- a/lib/renderful.rb +++ b/lib/renderful.rb @@ -6,7 +6,10 @@ require 'renderful/error/no_component_error' require 'renderful/cache/base' require 'renderful/cache/redis' -require 'renderful/cache_invalidator' +require 'renderful/cache/null' +require 'renderful/content_entry' +require 'renderful/provider/base' +require 'renderful/provider/contentful' require 'renderful/client' require 'renderful/component/base' require 'renderful/component/rails' diff --git a/lib/renderful/cache/base.rb b/lib/renderful/cache/base.rb index 7c1cd80..0042122 100644 --- a/lib/renderful/cache/base.rb +++ b/lib/renderful/cache/base.rb @@ -18,6 +18,10 @@ def write(_key, _value) def delete(_key) raise NotImplementedError end + + def fetch(key) + raise NotImplementedError + end end end end diff --git a/lib/renderful/cache/null.rb b/lib/renderful/cache/null.rb new file mode 100644 index 0000000..1e4791d --- /dev/null +++ b/lib/renderful/cache/null.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Renderful + module Cache + class Null < Base + def exist?(key) + false + end + + def read(key) + nil + end + + def write(key, value) + # noop + end + + def delete(key) + # noop + end + + def fetch(key) + nil + end + end + end +end diff --git a/lib/renderful/cache/redis.rb b/lib/renderful/cache/redis.rb index 5bffc63..8d93ee8 100644 --- a/lib/renderful/cache/redis.rb +++ b/lib/renderful/cache/redis.rb @@ -24,6 +24,14 @@ def write(key, value) def delete(key) redis.del(key) end + + def fetch(key) + return read(key) if exists?(key) + + yield.tap do |value| + write(key, value) + end + end end end end diff --git a/lib/renderful/cache_invalidator.rb b/lib/renderful/cache_invalidator.rb deleted file mode 100644 index 4b09298..0000000 --- a/lib/renderful/cache_invalidator.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -module Renderful - class CacheInvalidator - attr_reader :client - - def initialize(client) - @client = client - end - - def process_webhook(body) - return unless client.cache - - params = body.is_a?(String) ? JSON.parse(body) : body - - client.cache.delete(client.cache_key_for( - content_type_id: params['sys']['contentType']['sys']['id'], - entry_id: params['sys']['id'], - )) - - client.contentful.entries(links_to_entry: params['sys']['id']).each do |linking_entry| - client.cache.delete(client.cache_key_for(linking_entry)) - end - end - end -end diff --git a/lib/renderful/client.rb b/lib/renderful/client.rb index a2e5ec2..e2b2a5b 100644 --- a/lib/renderful/client.rb +++ b/lib/renderful/client.rb @@ -2,34 +2,37 @@ module Renderful class Client - attr_reader :contentful, :components, :cache + attr_reader :provider, :components, :cache - def initialize(contentful:, components:, cache: nil) - @contentful = contentful + def initialize(provider:, components:, cache: Cache::Null) + @provider = provider @components = components @cache = cache end - def render(entry) - component = components[entry.content_type.id] - fail(Error::NoComponentError, entry) unless component + def render(entry_id) + content_entry = ContentEntry.new(provider: provider, id: entry_id) - return cache.read(cache_key_for(entry)) if cache&.exist?(cache_key_for(entry)) - - component.new(entry, client: self).render.tap do |output| - cache&.write(cache_key_for(entry), output) + cache.fetch(content_entry.cache_key) do + content_entry.hydrate + component_for_entry(content_entry).render end end - def cache_key_for(entry) - if entry.respond_to?(:content_type) - cache_key_for( - content_type_id: entry.content_type.id, - entry_id: entry.id, - ) - else - "contentful/#{entry.fetch(:content_type_id)}/#{entry.fetch(:entry_id)}" + def invalidate_cache_from_webhook(body) + provider.cache_keys_to_invalidate(body).each do |cache_key| + cache.delete(cache_key) end end + + private + + def component_klass_for_entry(content_entry) + components[content_entry.content_type] || fail(Error::NoComponentError, content_entry) + end + + def component_for_entry(content_entry) + component_klass_for_entry(content_entry).new(content_entry, client: self) + end end end diff --git a/lib/renderful/content_entry.rb b/lib/renderful/content_entry.rb new file mode 100644 index 0000000..8a06f77 --- /dev/null +++ b/lib/renderful/content_entry.rb @@ -0,0 +1,25 @@ +module Renderful + class ContentEntry + attr_reader :provider, :id, :content_type, :fields + + def initialize(provider:, id:, content_type: nil, fields: {}) + @provider = provider + @id = id + @content_type = content_type + @fields = fields + end + + def cache_key + "renderful/#{provider.cache_prefix}/#{id}" + end + + def hydrate + other_entry = provider.find_entry(id) + + @content_type = other_entry.content_type + @fields = other_entry.fields + + self + end + end +end diff --git a/lib/renderful/error/no_component_error.rb b/lib/renderful/error/no_component_error.rb index b5167cd..066c080 100644 --- a/lib/renderful/error/no_component_error.rb +++ b/lib/renderful/error/no_component_error.rb @@ -8,7 +8,7 @@ class NoComponentError < Base def initialize(entry, *args) @entry = entry - super "Cannot find component for content type #{entry.content_type.id}", *args + super "Cannot find component for content type #{entry.content_type}", *args end end end diff --git a/lib/renderful/provider/base.rb b/lib/renderful/provider/base.rb index e69de29..070815a 100644 --- a/lib/renderful/provider/base.rb +++ b/lib/renderful/provider/base.rb @@ -0,0 +1,23 @@ +module Renderful + module Provider + class Base + attr_reader :options + + def initialize(options) + @options = options + end + + def cache_prefix + fail NotImplementedError + end + + def find_entry(entry_id) + fail NotImplementedError + end + + def cache_keys_to_invalidate(webhook_body) + fail NotImplementedError + end + end + end +end diff --git a/lib/renderful/provider/contentful.rb b/lib/renderful/provider/contentful.rb new file mode 100644 index 0000000..0b7f83e --- /dev/null +++ b/lib/renderful/provider/contentful.rb @@ -0,0 +1,52 @@ +module Renderful + module Provider + class Contentful < Base + def initialize(options) + super + + fail ArgumentError, 'contentful option is required!' unless contentful + end + + def cache_prefix + :contentful + end + + def find_entry(entry_id) + wrap_entry(contentful.entry(entry_id)) + end + + def cache_keys_to_invalidate(webhook_body) + params = webhook_body.is_a?(String) ? JSON.parse(webhook_body) : webhook_body + + modified_entry = ContentEntry.new( + provider: self, + content_type: params['sys']['contentType']['sys']['id'], + id: params['sys']['id'], + ) + + entries_to_invalidate = [modified_entry] + entries_linking_to(modified_entry) + + entries_to_invalidate.map(&:cache_key) + end + + private + + def wrap_entry(entry) + ContentEntry.new( + provider: self, + id: entry.id, + content_type: entry.content_type.id, + fields: entry.fields, + ) + end + + def entries_linking_to(entry) + contentful.entries(links_to_entry: entry.id).map(&method(:wrap_entry)) + end + + def contentful + options[:contentful] + end + end + end +end diff --git a/spec/renderful/cache_invalidator_spec.rb b/spec/renderful/cache_invalidator_spec.rb deleted file mode 100644 index eaaabc0..0000000 --- a/spec/renderful/cache_invalidator_spec.rb +++ /dev/null @@ -1,110 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Renderful::CacheInvalidator do - subject(:invalidator) { described_class.new(client) } - - let(:client) { instance_double('Renderful::Client', cache: cache) } - - describe '#process_webhook' do - let(:payload) do - <<~JSON - { - "sys": { - "space": { - "sys": { - "type": "Link", - "linkType": "Space", - "id": "uzfpz86mcpi5" - } - }, - "id": "6hxhiF6EcKklWANW9BBlVY", - "type": "Entry", - "createdAt": "2019-02-19T20:18:52.397Z", - "updatedAt": "2019-04-17T12:30:40.227Z", - "environment": { - "sys": { - "id": "master", - "type": "Link", - "linkType": "Environment" - } - }, - "createdBy": { - "sys": { - "type": "Link", - "linkType": "User", - "id": "7feCK4fgU19fUFFQsMcUmR" - } - }, - "updatedBy": { - "sys": { - "type": "Link", - "linkType": "User", - "id": "6N31zQDHY87IGFgJEJ030m" - } - }, - "publishedCounter": 0, - "version": 5, - "contentType": { - "sys": { - "type": "Link", - "linkType": "ContentType", - "id": "moduleImageBanner" - } - } - }, - "fields": { - "headline": { - "en-US": "Testing" - } - } - } - JSON - end - - context 'when caching is enabled on the client' do - let(:cache) { instance_spy('Renderful::Cache::Base') } - - before do - allow(client).to receive(:cache_key_for) - .with(content_type_id: 'moduleImageBanner', entry_id: '6hxhiF6EcKklWANW9BBlVY') - .and_return('contentful/moduleImageBanner/6hxhiF6EcKklWANW9BBlVY') - - contentful = instance_double('Contentful::Client') - allow(client).to receive(:contentful).and_return(contentful) - - linking_entry = instance_double('Contentful::Entry') - allow(contentful).to receive(:entries) - .with(links_to_entry: '6hxhiF6EcKklWANW9BBlVY') - .and_return([linking_entry]) - - allow(client).to receive(:cache_key_for) - .with(linking_entry) - .and_return('contentful/page/44292c56a87b9be07ac32b') - end - - it 'invalidates the cache for the updated entry' do - invalidator.process_webhook(payload) - - expect(cache).to have_received(:delete) - .with('contentful/moduleImageBanner/6hxhiF6EcKklWANW9BBlVY') - end - - it 'invalidates any entries linking to the updated entry' do - invalidator.process_webhook(payload) - - expect(cache).to have_received(:delete) - .with('contentful/page/44292c56a87b9be07ac32b') - end - end - - context 'when caching is disabled on the client' do - let(:cache) { nil } - - it 'exits early with no errors' do - expect { invalidator.process_webhook(payload) }.not_to raise_error - end - end - end -end diff --git a/spec/renderful/client_spec.rb b/spec/renderful/client_spec.rb index 13957d0..07d7c85 100644 --- a/spec/renderful/client_spec.rb +++ b/spec/renderful/client_spec.rb @@ -4,100 +4,81 @@ RSpec.describe Renderful::Client do subject(:client) do - described_class.new(contentful: contentful, components: components, cache: cache) + described_class.new( + provider: provider, + components: components, + cache: cache, + ) end - let(:contentful) { instance_double('Contentful::Client') } - let(:components) do - { - 'testContentType' => component_klass, - } - end - - let(:cache) { instance_spy('Renderful::Cache::Base') } + let(:provider) { instance_double('Renderful::Provider::Base', cache_prefix: :base) } + let(:cache) { instance_double('Renderful::Cache::Base') } + let(:content_type_id) { 'testContentType' } let(:component_klass) { class_double('Renderful::Component::Base') } - describe '#cache_key_for' do - context 'with an entry' do - let(:entry) do - OpenStruct.new( - id: 'entry_id', - content_type: OpenStruct.new(id: 'content_type_id'), - ) - end - - it 'returns a valid cache key' do - expect(client.cache_key_for(entry)).to eq('contentful/content_type_id/entry_id') - end - end - - context 'with an options hash' do - let(:options) { { entry_id: 'entry_id', content_type_id: 'content_type_id' } } + describe '#render' do + let(:entry_id) { 'entry_id' } - it 'returns a valid cache key' do - expect(client.cache_key_for(options)).to eq('contentful/content_type_id/entry_id') + context 'when the output has been cached' do + before do + allow(cache).to receive(:fetch) + .with(an_instance_of(String)) + .and_return('cached output') end - end - end - describe '#render' do - let(:entry) { OpenStruct.new(content_type: OpenStruct.new(id: content_type_id)) } - let(:cache) { instance_spy('Renderful::Cache::Base') } + context 'when a component has been registered for the provided content type' do + let(:components) { { content_type_id => component_klass } } - context 'when a component has been registered for the provided content type' do - let(:content_type_id) { 'testContentType' } - - context 'when the output has been cached' do - before do - allow(cache).to receive(:exist?) - .with(an_instance_of(String)) - .and_return(true) + it 'returns the cached output' do + result = client.render(entry_id) - allow(cache).to receive(:read) - .with(an_instance_of(String)) - .and_return('cached output') + expect(result).to eq('cached output') end + end + + context 'when no component has been registered for the provided content type' do + let(:components) { {} } it 'returns the cached output' do - result = client.render(entry) + result = client.render(entry_id) expect(result).to eq('cached output') end end + end - context 'when the output has not been cached' do - before do - allow(cache).to receive(:exist?) - .with(an_instance_of(String)) - .and_return(false) + context 'when the output has not been cached' do + let(:entry) { instance_double('ContentEntry', content_type: 'testContentType', fields: {}) } - allow(component_klass).to receive(:new) - .with(entry, client: client) - .and_return(instance_double('Renderful::Component::Base', render: 'render_output')) - end + before do + allow(cache).to receive(:fetch).with(an_instance_of(String)) { |_, &block| block.call } - it 'renders the content type with its component' do - result = client.render(entry) + allow(provider).to receive(:find_entry) + .with(an_instance_of(String)) + .and_return(entry) - expect(result).to eq('render_output') - end + allow(component_klass).to receive(:new) + .with(an_instance_of(Renderful::ContentEntry), client: client) + .and_return(instance_double('Renderful::Component::Base', render: 'render_output')) + end + + context 'when a component has been registered for the provided content type' do + let(:components) { { content_type_id => component_klass } } - it 'writes the output to the cache' do - client.render(entry) + it 'renders the content type with its component' do + result = client.render(entry_id) - expect(cache).to have_received(:write).with(an_instance_of(String), 'render_output') + expect(result).to eq('render_output') end end - end - context 'when no component has been registered for the provided content type' do - let(:content_type_id) { 'unknownContentType' } + context 'when no component has been registered for the provided content type' do + let(:components) { {} } - it 'raises a NoComponentError' do - expect { - client.render(entry) - }.to raise_error(Renderful::Error::NoComponentError) + it 'raises a NoComponentError' do + expect { client.render(entry_id) }.to raise_error(Renderful::Error::NoComponentError) + end end end end From 66571402858661982b4e41208dae7f9510956c46 Mon Sep 17 00:00:00 2001 From: Alessandro Desantis Date: Wed, 1 Apr 2020 17:16:28 +0200 Subject: [PATCH 5/9] Remove the ability to resolve references from components --- lib/renderful/component/base.rb | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/lib/renderful/component/base.rb b/lib/renderful/component/base.rb index 96ed173..a0014e2 100644 --- a/lib/renderful/component/base.rb +++ b/lib/renderful/component/base.rb @@ -5,14 +5,6 @@ module Component class Base attr_reader :entry, :client - class << self - def resolve(field) - define_method(field) do - resolve(entry.send(field)) - end - end - end - def initialize(entry, client:) @entry = entry @client = client @@ -21,18 +13,6 @@ def initialize(entry, client:) def render raise NotImplementedError end - - private - - def resolve(reference) - if reference.is_a?(Enumerable) - reference.map(&method(:resolve)) - elsif reference.is_a?(Contentful::Link) - reference.resolve(client.contentful) - else - reference - end - end end end end From 95c0f706bd381f404a022f244816b5879fae3b07 Mon Sep 17 00:00:00 2001 From: Alessandro Desantis Date: Wed, 1 Apr 2020 17:16:56 +0200 Subject: [PATCH 6/9] Fix coding style issues --- lib/renderful/cache/base.rb | 2 +- lib/renderful/cache/null.rb | 6 +++--- lib/renderful/content_entry.rb | 2 ++ lib/renderful/error/base.rb | 2 ++ lib/renderful/provider/base.rb | 6 ++++-- lib/renderful/provider/contentful.rb | 2 ++ 6 files changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/renderful/cache/base.rb b/lib/renderful/cache/base.rb index 0042122..242608e 100644 --- a/lib/renderful/cache/base.rb +++ b/lib/renderful/cache/base.rb @@ -19,7 +19,7 @@ def delete(_key) raise NotImplementedError end - def fetch(key) + def fetch(_key) raise NotImplementedError end end diff --git a/lib/renderful/cache/null.rb b/lib/renderful/cache/null.rb index 1e4791d..bc6b049 100644 --- a/lib/renderful/cache/null.rb +++ b/lib/renderful/cache/null.rb @@ -3,11 +3,11 @@ module Renderful module Cache class Null < Base - def exist?(key) + def exist?(_key) false end - def read(key) + def read(_key) nil end @@ -19,7 +19,7 @@ def delete(key) # noop end - def fetch(key) + def fetch(_key) nil end end diff --git a/lib/renderful/content_entry.rb b/lib/renderful/content_entry.rb index 8a06f77..cb58c75 100644 --- a/lib/renderful/content_entry.rb +++ b/lib/renderful/content_entry.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Renderful class ContentEntry attr_reader :provider, :id, :content_type, :fields diff --git a/lib/renderful/error/base.rb b/lib/renderful/error/base.rb index 2943da6..0004ba7 100644 --- a/lib/renderful/error/base.rb +++ b/lib/renderful/error/base.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Renderful module Error class Base < StandardError; end diff --git a/lib/renderful/provider/base.rb b/lib/renderful/provider/base.rb index 070815a..41e7bc4 100644 --- a/lib/renderful/provider/base.rb +++ b/lib/renderful/provider/base.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Renderful module Provider class Base @@ -11,11 +13,11 @@ def cache_prefix fail NotImplementedError end - def find_entry(entry_id) + def find_entry(_entry_id) fail NotImplementedError end - def cache_keys_to_invalidate(webhook_body) + def cache_keys_to_invalidate(_webhook_body) fail NotImplementedError end end diff --git a/lib/renderful/provider/contentful.rb b/lib/renderful/provider/contentful.rb index 0b7f83e..1f01731 100644 --- a/lib/renderful/provider/contentful.rb +++ b/lib/renderful/provider/contentful.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Renderful module Provider class Contentful < Base From 945a97550797bb3ff01050963d748d05547044c5 Mon Sep 17 00:00:00 2001 From: Alessandro Desantis Date: Wed, 1 Apr 2020 17:36:13 +0200 Subject: [PATCH 7/9] Update documentation --- README.md | 130 +++++++++++------------------------------------------- 1 file changed, 26 insertions(+), 104 deletions(-) diff --git a/README.md b/README.md index 1384bc7..f3f9ee8 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ [![CircleCI](https://circleci.com/gh/nebulab/renderful.svg?style=svg)](https://circleci.com/gh/nebulab/renderful) -Welcome! Renderful is a rendering engine for [Contentful](https://www.contentful.com) spaces. It -allows you to map your content types to Ruby objects that take care of rendering your content. +Welcome! Renderful is a rendering engine for headless CMSs. It allows you to map your content types +to Ruby objects that take care of rendering your content. ## Installation @@ -28,16 +28,16 @@ $ gem install renderful Once you have installed the gem, you can configure it like this: ```ruby -contentful = Contentful::Client.new( - space: 'CONTENTFUL_SPACE_ID', - access_token: 'CONTENTFUL_ACCESS_TOKEN', -) - -renderful = Renderful.new( - contentful: contentful, +RenderfulClient = Renderful::Client.new( + provider: Renderful::Provider::Contentful.new( + contentful: Contentful::Client.new( + space: 'YOUR_SPACE_ID', + access_key: 'YOUR_ACCESS_KEY', + ), + ), components: { 'jumbotron' => JumbotronComponent, - } + }, ) ``` @@ -53,86 +53,24 @@ class JumbotronComponent < Renderful::Component def render <<~HTML
-

<%= entry.title %>

-

<%= entry.content %>

+

<%= entry.fields[:title] %>

+

<%= entry.fields[:content] %>

HTML end end ``` -You can now render this component by retrieving it from Contentful and rendering it with Renderful: - -```ruby -entry = contentful.entry('jumbotron_entry_id') -renderful.render(entry) -``` - -### Rich text rendering - -If you have rich-text fields, you can leverage Contentful's [rich_text_renderer](https://github.com/contentful/rich-text-renderer.rb) -along with a custom local variable: - -```ruby -class TextBlockComponent < Renderful::Component::Rails - def html_body - RichTextRenderer::Renderer.new.render(entry.body) - end - - def locals - { html_body: html_body } - end -end -``` - -Then, just reference the `html_body` variable as usual: - -```erb -<%# app/views/renderful/_text_block.html.erb %> -<%= raw html_body %> -``` - -### Nested components - -What if you want to have a `Grid` component that can contain references to other components? It's -actually quite simple! Simply create a _References_ field for your content, then recursively render -all of the content entries contained in that field: +You can now render this component like this: ```ruby -# app/components/grid.rb -class Grid < Renderful::Component - # This will define a `resolved_blocks` method that reads external references - # from the `blocks` fields and turns them into Contentful::Entry instances - resolve :blocks - - def render - entries = blocks.map do |block| - # `client` can be used to access the Renderful::Client instance - <<~HTML -
- #{client.render(block)} -
- HTML - end - - <<~HTML -
#{entries}
- HTML - end -end +RenderfulClient.render('my_entry_id') ``` ### Caching -You can easily cache the output of your components by passing a `cache` key when instantiating the -client. The value of this key should be an object that responds to the following methods: - -- `#read(key)` -- `#write(key, value)` -- `#delete(key)` -- `#exist?(key)` - -A Redis cache implementation is included out of the box. Here's an example: +You can easily cache the output of your components. A Redis cache implementation is included out of +the box. Here's an example: ```ruby renderful = Renderful.new( @@ -159,23 +97,23 @@ renderful = Renderful.new( #### Cache invalidation -The best way to invalidate the cache is through [Contentful webhooks](https://www.contentful.com/developers/docs/concepts/webhooks/). +The best way to invalidate the cache is through [webhooks](https://www.contentful.com/developers/docs/concepts/webhooks/). Renderful ships with a framework-agnostic webhook processor you can use to automatically invalidate the cache for all updated content: ```ruby -Renderful::CacheInvalidator.new(renderful).process_webhook(json_body) +RenderfulClient.invalidate_cache_from_webhook(json_body) ``` This is how you could use it in a Rails controller: ```ruby -class ContentfulWebhooksController < ApplicationController +class WebhooksController < ApplicationController skip_before_action :verify_authenticity_token def create - Renderful::CacheInvalidator.new(RenderfulClient).process_webhook(request.raw_post) + RenderfulClient.invalidate_cache_from_webhook(request.raw_post) head :no_content end end @@ -200,12 +138,12 @@ Then, create an `app/views/renderful/_jumbotron.html.erb` partial: ```erb
-

<%= entry.title %>

-

<%= entry.content %>

+

<%= entry.fields[:title] %>

+

<%= entry.fields[:content] %>

``` -As you can see, you can access the Contentful entry via the `entry` local variable. +As you can see, you can access the content entry via the `entry` local variable. #### Custom renderer @@ -239,26 +177,10 @@ You would then access them like regular locals: ```erb

- <%= entry.title %> - (<%= italian_title %>) + <%= entry.fields[:title] %> + (<%= italian_title %>)

-

<%= entry.content %>

-
-``` - -#### Resolution in ERB views - -If you need to render resolved fields (as in our `Grid` example), you can use `component` and -`client` to access the `Renderful::Component` and `Renderful::Client` objects: - -```erb -<%# app/views/renderful/_grid.html.erb %> -
- <% component.blocks.each do |block| %> -
- <%= client.render(block) %> -
- <% end %> +

<%= entry.fields[:content] %>

``` From 8926c8aeb7eb94a92031c1be896a48049def6bb2 Mon Sep 17 00:00:00 2001 From: Alessandro Desantis Date: Wed, 1 Apr 2020 17:38:30 +0200 Subject: [PATCH 8/9] Fix CircleCI not installing all gems --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index dca8cb9..a41bd6d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -15,7 +15,7 @@ jobs: - renderful-v1- - run: name: Bundle Install - command: bundle check || bundle install + command: bundle exec appraisal install - save_cache: key: renderful-v1-{{ .Branch }}-{{ .Revision }} paths: From b415c12d168306716b7a89c23decaad35ffc3c1d Mon Sep 17 00:00:00 2001 From: Alessandro Desantis Date: Wed, 1 Apr 2020 18:15:45 +0200 Subject: [PATCH 9/9] Write missing unit tests --- lib/renderful/cache/null.rb | 2 +- lib/renderful/cache/redis.rb | 2 +- lib/renderful/component/rails.rb | 2 +- spec/renderful/cache/null_spec.rb | 37 ++++++ spec/renderful/cache/redis_spec.rb | 34 ++++++ spec/renderful/client_spec.rb | 19 +++ spec/renderful/component/rails_spec.rb | 6 +- spec/renderful/content_entry_spec.rb | 49 ++++++++ spec/renderful/provider/contentful_spec.rb | 134 +++++++++++++++++++++ 9 files changed, 278 insertions(+), 7 deletions(-) create mode 100644 spec/renderful/cache/null_spec.rb create mode 100644 spec/renderful/content_entry_spec.rb create mode 100644 spec/renderful/provider/contentful_spec.rb diff --git a/lib/renderful/cache/null.rb b/lib/renderful/cache/null.rb index bc6b049..aa75cd4 100644 --- a/lib/renderful/cache/null.rb +++ b/lib/renderful/cache/null.rb @@ -20,7 +20,7 @@ def delete(key) end def fetch(_key) - nil + yield end end end diff --git a/lib/renderful/cache/redis.rb b/lib/renderful/cache/redis.rb index 8d93ee8..e78bd24 100644 --- a/lib/renderful/cache/redis.rb +++ b/lib/renderful/cache/redis.rb @@ -26,7 +26,7 @@ def delete(key) end def fetch(key) - return read(key) if exists?(key) + return read(key) if exist?(key) yield.tap do |value| write(key, value) diff --git a/lib/renderful/component/rails.rb b/lib/renderful/component/rails.rb index 04d6c06..5df45b1 100644 --- a/lib/renderful/component/rails.rb +++ b/lib/renderful/component/rails.rb @@ -18,7 +18,7 @@ def locals end def view - "renderful/#{entry.content_type.id.demodulize.underscore}" + "renderful/#{entry.content_type.demodulize.underscore}" end def default_locals diff --git a/spec/renderful/cache/null_spec.rb b/spec/renderful/cache/null_spec.rb new file mode 100644 index 0000000..ecb8f3d --- /dev/null +++ b/spec/renderful/cache/null_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Renderful::Cache::Null do + subject(:cache) { described_class.new } + + describe '#exist?' do + it 'returns false' do + expect(cache.exist?('key')).to eq(false) + end + end + + describe '#write' do + it 'is a no-op' do + expect { cache.write('key', 'value') }.not_to raise_error + end + end + + describe '#read' do + it 'returns nil' do + expect(cache.read('key')).to eq(nil) + end + end + + describe '#delete' do + it 'is a no-op' do + expect { cache.delete('key') }.not_to raise_error + end + end + + describe '#fetch' do + it 'always yields' do + expect(cache.fetch('key') { 'value' }).to eq('value') + end + end +end diff --git a/spec/renderful/cache/redis_spec.rb b/spec/renderful/cache/redis_spec.rb index efbbb2c..134091f 100644 --- a/spec/renderful/cache/redis_spec.rb +++ b/spec/renderful/cache/redis_spec.rb @@ -60,4 +60,38 @@ expect(redis).to have_received(:del).with('key') end end + + describe '#fetch' do + context 'when the key exists in Redis' do + before do + allow(redis).to receive(:exists) + .with('key') + .and_return(true) + + allow(redis).to receive(:get).with('key').and_return('value') + end + + it 'returns the stored value' do + expect(cache.fetch('key') { fail StandardError }).to eq('value') + end + end + + context 'when the key does not exist in Redis' do + before do + allow(redis).to receive(:exists) + .with('key') + .and_return(false) + end + + it 'writes the key to Redis' do + cache.fetch('key') { 'value' } + + expect(redis).to have_received(:set).with('key', 'value') + end + + it 'returns the value' do + expect(cache.fetch('key') { 'value' }).to eq('value') + end + end + end end diff --git a/spec/renderful/client_spec.rb b/spec/renderful/client_spec.rb index 07d7c85..c72f682 100644 --- a/spec/renderful/client_spec.rb +++ b/spec/renderful/client_spec.rb @@ -12,6 +12,7 @@ end let(:provider) { instance_double('Renderful::Provider::Base', cache_prefix: :base) } + let(:components) { {} } let(:cache) { instance_double('Renderful::Cache::Base') } let(:content_type_id) { 'testContentType' } @@ -82,4 +83,22 @@ end end end + + describe '#invalidate_cache_from_webhook' do + let(:cache) { instance_spy('Renderful::Cache::Base') } + + let(:payload) { 'dummy payload' } + + before do + allow(provider).to receive(:cache_keys_to_invalidate) + .with(payload) + .and_return(%w[key1]) + end + + it 'invalidates the cache keys returned by the provider' do + client.invalidate_cache_from_webhook(payload) + + expect(cache).to have_received(:delete).with('key1') + end + end end diff --git a/spec/renderful/component/rails_spec.rb b/spec/renderful/component/rails_spec.rb index 23838f6..50680f6 100644 --- a/spec/renderful/component/rails_spec.rb +++ b/spec/renderful/component/rails_spec.rb @@ -3,11 +3,9 @@ require 'spec_helper' RSpec.describe Renderful::Component::Rails do - subject(:component) do - TestComponent.new(entry, client: client) - end + subject(:component) { TestComponent.new(entry, client: client) } - let(:entry) { OpenStruct.new(content_type: OpenStruct.new(id: 'test')) } + let(:entry) { instance_double('ContentEntry', content_type: 'test') } let(:client) { instance_double('Renderful::Client') } before(:all) do diff --git a/spec/renderful/content_entry_spec.rb b/spec/renderful/content_entry_spec.rb new file mode 100644 index 0000000..22ab40f --- /dev/null +++ b/spec/renderful/content_entry_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Renderful::ContentEntry do + subject { described_class.new(provider: provider, id: id, content_type: content_type, fields: fields) } + + let(:provider) { instance_double('Renderful::Provider::Base') } + let(:id) { :content_entry_id } + let(:content_type) { nil } + let(:fields) { {} } + + describe '#cache_key' do + before do + allow(provider).to receive(:cache_prefix).and_return('dummy_provider') + end + + it 'generates a cache key for the content entry' do + expect(subject.cache_key).to eq('renderful/dummy_provider/content_entry_id') + end + end + + describe '#hydrate' do + before do + allow(provider).to receive(:find_entry).with(id).and_return(described_class.new( + provider: provider, + id: id, + content_type: 'new_content_type', + fields: { 'new' => 'fields' }, + )) + end + + it 'hydrates the content entry with the content type from the CMS' do + subject.hydrate + + expect(subject.content_type).to eq('new_content_type') + end + + it 'hydrates the content entry with the fields from the CMS' do + subject.hydrate + + expect(subject.fields).to eq('new' => 'fields') + end + + it 'returns self' do + expect(subject.hydrate).to eq(subject) + end + end +end diff --git a/spec/renderful/provider/contentful_spec.rb b/spec/renderful/provider/contentful_spec.rb new file mode 100644 index 0000000..6b77a06 --- /dev/null +++ b/spec/renderful/provider/contentful_spec.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Renderful::Provider::Contentful do + subject { described_class.new(contentful: contentful) } + + let(:contentful) { instance_double('Contentful::Client') } + + describe '#cache_prefix' do + it 'returns contentful' do + expect(subject.cache_prefix).to eq(:contentful) + end + end + + describe '#find_entry' do + before do + allow(contentful).to receive(:entry) + .with(entry_id) + .and_return(OpenStruct.new( + fields: { 'foo' => 'bar' }, + content_type: OpenStruct.new(id: 'test_content_type'), + id: entry_id, + )) + end + + let(:entry_id) { 'test_entry_id' } + + it 'maps Contentful fields to the content entry' do + content_entry = subject.find_entry(entry_id) + + expect(content_entry.fields).to eq('foo' => 'bar') + end + + it 'maps the Contentful content type ID to the content entry' do + content_entry = subject.find_entry(entry_id) + + expect(content_entry.content_type).to eq('test_content_type') + end + + it 'maps the Contentful entry ID to the content entry' do + content_entry = subject.find_entry(entry_id) + + expect(content_entry.id).to eq(entry_id) + end + + it 'sets the provider on the content entry' do + content_entry = subject.find_entry(entry_id) + + expect(content_entry.provider).to eq(subject) + end + end + + describe '#cache_keys_to_invalidate' do + let(:payload) do + <<~JSON + { + "sys": { + "space": { + "sys": { + "type": "Link", + "linkType": "Space", + "id": "uzfpz86mcpi5" + } + }, + "id": "6hxhiF6EcKklWANW9BBlVY", + "type": "Entry", + "createdAt": "2019-02-19T20:18:52.397Z", + "updatedAt": "2019-04-17T12:30:40.227Z", + "environment": { + "sys": { + "id": "master", + "type": "Link", + "linkType": "Environment" + } + }, + "createdBy": { + "sys": { + "type": "Link", + "linkType": "User", + "id": "7feCK4fgU19fUFFQsMcUmR" + } + }, + "updatedBy": { + "sys": { + "type": "Link", + "linkType": "User", + "id": "6N31zQDHY87IGFgJEJ030m" + } + }, + "publishedCounter": 0, + "version": 5, + "contentType": { + "sys": { + "type": "Link", + "linkType": "ContentType", + "id": "moduleImageBanner" + } + } + }, + "fields": { + "headline": { + "en-US": "Testing" + } + } + } + JSON + end + + before do + allow(contentful).to receive(:entries) + .with(links_to_entry: '6hxhiF6EcKklWANW9BBlVY') + .and_return([OpenStruct.new( + fields: { 'foo' => 'bar' }, + content_type: OpenStruct.new(id: 'test_content_type'), + id: 'linking_entry_id', + )]) + + allow(Renderful::ContentEntry).to receive(:new).with(a_hash_including(id: '6hxhiF6EcKklWANW9BBlVY')) + .and_return(instance_double('ContentEntry', id: '6hxhiF6EcKklWANW9BBlVY', cache_key: 'cache/6hxhiF6EcKklWANW9BBlVY')) + + allow(Renderful::ContentEntry).to receive(:new).with(a_hash_including(id: 'linking_entry_id')) + .and_return(instance_double('ContentEntry', id: 'linking_entry_id', cache_key: 'cache/linking_entry_id')) + end + + it 'returns the cache key for the invalidated entry' do + expect(subject.cache_keys_to_invalidate(payload)).to include('cache/6hxhiF6EcKklWANW9BBlVY') + end + + it 'returns the cache keys for any entries linking to the invalidated one' do + expect(subject.cache_keys_to_invalidate(payload)).to include('cache/linking_entry_id') + end + end +end