lib/wcc/contentful.rb in wcc-contentful-0.1.0 vs lib/wcc/contentful.rb in wcc-contentful-0.2.0

- old
+ new

@@ -13,33 +13,84 @@ require 'wcc/contentful/content_type_indexer' require 'wcc/contentful/model_validators' require 'wcc/contentful/model' require 'wcc/contentful/model_builder' +## +# The root namespace of the wcc-contentful gem +# +# Initialize the gem with the `configure` and `init` methods inside your +# initializer. module WCC::Contentful class << self + ## + # Gets the current configuration, after calling WCC::Contentful.configure attr_reader :configuration + + ## + # Gets the sync token that was returned by the Contentful CDN after the most + # recent invocation of WCC::Contentful.sync! + attr_reader :next_sync_token end + ## + # Gets a {CDN Client}[rdoc-ref:WCC::Contentful::SimpleClient::Cdn] which provides + # methods for getting and paging raw JSON data from the Contentful CDN. def self.client configuration&.client end + ## + # Gets the data-store which executes the queries run against the dynamic + # models in the WCC::Contentful::Model namespace. + # This is one of the following based on the configured content_delivery method: + # + # [:direct] an instance of WCC::Contentful::Store::CDNAdapter with a + # {CDN Client}[rdoc-ref:WCC::Contentful::SimpleClient::Cdn] to access the CDN. + # + # [:lazy_sync] an instance of WCC::Contentful::Store::LazyCacheStore + # with the configured ActiveSupport::Cache implementation and a + # {CDN Client}[rdoc-ref:WCC::Contentful::SimpleClient::Cdn] for when data + # cannot be found in the cache. + # + # [:eager_sync] an instance of the configured Store type, defined by + # WCC::Contentful::Configuration.sync_store + # + def self.store + WCC::Contentful::Model.store + end + + ## + # Configures the WCC::Contentful gem to talk to a Contentful space. + # This must be called first in your initializer, before #init! or accessing the + # client. def self.configure @configuration ||= Configuration.new + @next_sync_token = nil yield(configuration) - configuration.configure_contentful - - raise ArgumentError, 'Please provide "space" ID' unless configuration.space.present? + raise ArgumentError, 'Please provide "space"' unless configuration.space.present? raise ArgumentError, 'Please provide "access_token"' unless configuration.access_token.present? + configuration.configure_contentful + configuration end + ## + # Initializes the WCC::Contentful model-space and backing store. + # This populates the WCC::Contentful::Model namespace with Ruby classes + # that represent content types in the configured Contentful space. + # + # These content types can be queried directly: + # WCC::Contentful::Model::Page.find('1xab...') + # Or you can inherit from them in your own app: + # class Page < WCC::Contentful::Model.page; end + # Page.find_by(slug: 'about-us') def self.init! raise ArgumentError, 'Please first call WCC:Contentful.configure' if configuration.nil? + @mutex ||= Mutex.new # we want as much as possible the raw JSON from the API content_types_resp = if configuration.management_client configuration.management_client.content_types(limit: 1000) @@ -52,22 +103,16 @@ ContentTypeIndexer.new.tap do |ixr| @content_types.each { |type| ixr.index(type) } end @types = indexer.types - case configuration.content_delivery - when :eager_sync - store = configuration.sync_store + store = configuration.store + WCC::Contentful::Model.store = store - client.sync(initial: true).items.each do |item| - # TODO: enrich existing type data using Sync::Indexer - store.index(item.dig('sys', 'id'), item) - end - WCC::Contentful::Model.store = store - when :direct - store = Store::CDNAdapter.new(client) - WCC::Contentful::Model.store = store + if store.respond_to?(:index) + @next_sync_token = store.find("sync:#{configuration.space}:token") + sync! end WCC::Contentful::ModelBuilder.new(@types).build_models # Extend all model types w/ validation & extra fields @@ -75,27 +120,71 @@ file = File.dirname(__FILE__) + "/contentful/model/#{t.name.underscore}.rb" require file if File.exist?(file) end return unless defined?(Rails) - Dir[Rails.root.join('lib/wcc/contentful/model/**/*.rb')].each { |file| require file } + + # load up the engine so it gets automatically mounted + require 'wcc/contentful/engine' end + ## + # Runs validations over the content types returned from the Contentful API. + # Validations are configured on predefined model classes using the + # `validate_field` directive. Example: + # validate_field :top_button, :Link, :optional, link_to: 'menuButton' + # This results in a WCC::Contentful::ValidationError + # if the 'topButton' field in the 'menu' content type is not a link. def self.validate_models! - schema = - Dry::Validation.Schema do - WCC::Contentful::Model.all_models.each do |klass| - next unless klass.schema - ct = klass.try(:content_type) || klass.name.demodulize - required(ct).schema(klass.schema) - end - end + # Ensure application models are loaded before we validate + Dir[Rails.root.join('app/models/**/*.rb')].each { |file| require file } if defined?(Rails) content_types = WCC::Contentful::ModelValidators.transform_content_types_for_validation( @content_types ) - errors = schema.call(content_types) + errors = WCC::Contentful::Model.schema.call(content_types) raise WCC::Contentful::ValidationError, errors.errors unless errors.success? + end + + ## + # Calls the Contentful Sync API and updates the configured store with the returned + # data. + # + # up_to_id: An ID that we know has changed and should come back from the sync. + # If we don't find this ID in the sync data, then drop a job to try + # the sync again after a few minutes. + # + def self.sync!(up_to_id: nil) + return unless store.respond_to?(:index) + + @mutex.synchronize do + sync_resp = client.sync(sync_token: next_sync_token) + + id_found = up_to_id.nil? + + sync_resp.items.each do |item| + id = item.dig('sys', 'id') + id_found ||= id == up_to_id + store.index(item) + end + store.set("sync:#{configuration.space}:token", sync_resp.next_sync_token) + @next_sync_token = sync_resp.next_sync_token + + unless id_found + raise SyncError, "ID '#{up_to_id}' did not come back via sync." unless defined?(Rails) + sync_later!(up_to_id: up_to_id) + end + next_sync_token + end + end + + ## + # Drops an ActiveJob job to invoke WCC::Contentful.sync! after a given amount + # of time. + def self.sync_later!(up_to_id: nil, wait: 10.minutes) + raise NotImplementedError, 'Cannot sync_later! outside of a Rails app' unless defined?(Rails) + + WCC::Contentful::DelayedSyncJob.set(wait: wait).perform_later(up_to_id) end # TODO: https://zube.io/watermarkchurch/development/c/2234 init graphql # def self.init_graphql! # require 'wcc/contentful/graphql'