Documentation by YARD 0.9.28
+Alphabetic Index
+ +File Listing
+-
+
+
+
- README + + +
Namespace Listing A-Z
+ + + + ++ + | +
diff --git a/docs/1.3/_index.html b/docs/1.3/_index.html new file mode 100644 index 00000000..b0d24aaf --- /dev/null +++ b/docs/1.3/_index.html @@ -0,0 +1,94 @@ + + +
+ + ++ + | +
t |
+ + + +4 +5 +6 +7 +8 +9 +10 +11+ |
+
+ # File 'lib/generators/wcc/templates/section-contact-form/migrations/create_wcc_contentful_app_contact_form_submissions.rb', line 4 + +def change + create_table :wcc_contentful_app_contact_form_submissions do |t| + t.string :form_id + t.json :data, default: {} + + t. + end +end+ |
+
This model represents the 'dropdownMenu' content type in Contentful. Any linked entries of the 'dropdownMenu' content type will be resolved as instances of this class. It exposes #find, #find_by, and #find_all methods to query Contentful.
+ + +This model represents the 'formField' content type in Contentful. Any linked entries of the 'formField' content type will be resolved as instances of this class. It exposes #find, #find_by, and #find_all methods to query Contentful.
+ + +This model represents the 'menu' content type in Contentful. Any linked entries of the 'menu' content type will be resolved as instances of this class. It exposes #find, #find_by, and #find_all methods to query Contentful.
+ + +This model represents the 'menuButton' content type in Contentful. Any linked entries of the 'menuButton' content type will be resolved as instances of this class. It exposes #find, #find_by, and #find_all methods to query Contentful.
+ + +This model represents the 'page' content type in Contentful. Any linked entries of the 'page' content type will be resolved as instances of this class. It exposes #find, #find_by, and #find_all methods to query Contentful.
+ + +This model represents the 'redirect' content type in Contentful. Any linked entries of the 'redirect' content type will be resolved as instances of this class. It exposes .find, .find_by, and .find_all methods to query Contentful.
+ + +This model represents the 'section-block-text' content type in Contentful. Any linked entries of the 'section-block-text' content type will be resolved as instances of this class. It exposes .find, .find_by, and .find_all methods to query Contentful.
+ + +This model represents the 'section-code-widget' content type in Contentful. Any linked entries of the 'section-code-widget' content type will be resolved as instances of this class. It exposes .find, .find_by, and .find_all methods to query Contentful.
+ + +This model represents the 'section-contact-form' content type in Contentful. Any linked entries of the 'section-contact-form' content type will be resolved as instances of this class. It exposes .find, .find_by, and .find_all methods to query Contentful.
+ + +This model represents the 'section-faq' content type in Contentful. Any linked entries of the 'section-faq' content type will be resolved as instances of this class. It exposes .find, .find_by, and .find_all methods to query Contentful.
+ + +This model represents the 'section-http-error' content type in Contentful. Any linked entries of the 'section-http-error' content type will be resolved as instances of this class. It exposes .find, .find_by, and .find_all methods to query Contentful.
+ + +This model represents the 'section-marquee-text' content type in Contentful. Any linked entries of the 'section-marquee-text' content type will be resolved as instances of this class. It exposes .find, .find_by, and .find_all methods to query Contentful.
+ + +This model represents the 'section-testimonials' content type in Contentful. Any linked entries of the 'section-testimonials' content type will be resolved as instances of this class. It exposes .find, .find_by, and .find_all methods to query Contentful.
+ + +This model represents the 'section-video' content type in Contentful. Any linked entries of the 'section-video' content type will be resolved as instances of this class. It exposes .find, .find_by, and .find_all methods to query Contentful.
+ + +This model represents the 'section-video-highlight' content type in Contentful. Any linked entries of the 'section-video-highlight' content type will be resolved as instances of this class. It exposes .find, .find_by, and .find_all methods to query Contentful.
+ + +This model represents the 'site-config' content type in Contentful. Any linked entries of the 'site-config' content type will be resolved as instances of this class. It exposes .find, .find_by, and .find_all methods to query Contentful.
+ + ++ + + Modules: Contentful + + + + +
+ + + + + + + + + ++ + + Modules: App + + + + +
+ + + + + + + + + ++ + + Modules: MenuHelper, PreviewPassword, SectionHelper + + + + Classes: Configuration, ContactFormController, ContactFormSubmission, ContactMailer, CustomMarkdownRender, Engine, MarkdownRenderer, PageNotFoundError, PagesController, ValidationError + + +
+ + +'1.3.0'
Gets the current configuration, after calling WCC::Contentful::App.configure.
+Returns the value of attribute initialized.
+Gets the current configuration, after calling WCC::Contentful::App.configure
+ + +
+ + + +14 +15 +16+ |
+
+ # File 'lib/wcc/contentful/app.rb', line 14 + +def configuration + @configuration +end+ |
+
Returns the value of attribute initialized.
+ + +
+ + + +11 +12 +13+ |
+
+ # File 'lib/wcc/contentful/app.rb', line 11 + +def initialized + @initialized +end+ |
+
+ + + +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32+ |
+
+ # File 'lib/wcc/contentful/app.rb', line 17 + +def self.configure + if initialized || WCC::Contentful.initialized + raise WCC::Contentful::InitializationError, 'Cannot configure after initialization' + end + + WCC::Contentful.configure do |wcc_contentful_config| + if @configuration&.wcc_contentful_config != wcc_contentful_config + @configuration = Configuration.new(wcc_contentful_config) + end + yield(configuration) + end + + configuration.validate! + + configuration +end+ |
+
+ + + +59 +60 +61+ |
+
+ # File 'lib/wcc/contentful/app.rb', line 59 + +def self.db_connected? + @db_connected +end+ |
+
+ + + +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57+ |
+
+ # File 'lib/wcc/contentful/app.rb', line 34 + +def self.init! + raise ArgumentError, 'Please first call WCC::Contentful::App.configure' if configuration.nil? + + WCC::Contentful.init! + + # Extend all model types w/ validation & extra fields + WCC::Contentful::Model.schema.each_value do |t| + file = File.dirname(__FILE__) + "/model/#{t.name.underscore}.rb" + require file if File.exist?(file) + end + + @db_connected = + begin + ::ActiveRecord::Base.connection_pool.with_connection(&:active?) + rescue StandardError + false + end + + @configuration = WCC::Contentful::App::Configuration::FrozenConfiguration.new( + configuration, + WCC::Contentful.configuration + ) + @initialized = true +end+ |
+
This object contains all the configuration options for the `wcc-contentful` gem.
+ + ++ + + + + Classes: FrozenConfiguration + + +
+ + +TODO: things to configure in the app?
+ + +%i[ + preview_password +].freeze
Sets the password that will be checked when the query string contains `preview=`, if it matches, then the Contentful entries are fetched via the preview API.
+Returns the value of attribute wcc_contentful_config.
+A new instance of Configuration.
+Validates the configuration, raising ArgumentError if anything is wrong.
+Returns a new instance of Configuration.
+ + +
+ + + +20 +21 +22 +23+ |
+
+ # File 'lib/wcc/contentful/app/configuration.rb', line 20 + +def initialize(wcc_contentful_config) + @wcc_contentful_config = wcc_contentful_config + @preview_password = ENV.fetch('CONTENTFUL_PREVIEW_PASSWORD', nil) +end+ |
+
Sets the password that will be checked when the query string contains `preview=`, if it matches, then the Contentful entries are fetched via the preview API.
+ + +
+ + + +12 +13 +14+ |
+
+ # File 'lib/wcc/contentful/app/configuration.rb', line 12 + +def preview_password + @preview_password +end+ |
+
Returns the value of attribute wcc_contentful_config.
+ + +
+ + + +14 +15 +16+ |
+
+ # File 'lib/wcc/contentful/app/configuration.rb', line 14 + +def wcc_contentful_config + @wcc_contentful_config +end+ |
+
+ + + +31 +32 +33+ |
+
+ # File 'lib/wcc/contentful/app/configuration.rb', line 31 + +def frozen? + false +end+ |
+
Validates the configuration, raising ArgumentError if anything is wrong. This is called by WCC::Contentful::App.init!
+ + +
+ + + +27 +28 +29+ |
+
+ # File 'lib/wcc/contentful/app/configuration.rb', line 27 + +def validate! + wcc_contentful_config.validate! +end+ |
+
A new instance of FrozenConfiguration.
+Returns a new instance of FrozenConfiguration.
+ + +
+ + + +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50+ |
+
+ # File 'lib/wcc/contentful/app/configuration.rb', line 40 + +def initialize(configuration, frozen_wcc_contentful_config) + raise ArgumentError, 'Please first freeze the wcc_contentful_config' unless frozen_wcc_contentful_config.frozen? + + @wcc_contentful_config = frozen_wcc_contentful_config + + ATTRIBUTES.each do |att| + val = configuration.public_send(att) + val.freeze if val.respond_to?(:freeze) + instance_variable_set("@#{att}", val) + end +end+ |
+
+ + + +52 +53 +54+ |
+
+ # File 'lib/wcc/contentful/app/configuration.rb', line 52 + +def frozen? + true +end+ |
+
+ + + +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20+ |
+
+ # File 'app/controllers/wcc/contentful/app/contact_form_controller.rb', line 6 + +def create + address = + form_model.to_address(email_object_id: params[:email_object_id]) + + form_model.send_email( + form_params.merge!( + { + notification_email: address, + internal_title: params[:internal_title] + } + ) + ) + + render json: { type: 'success', message: "Thanks for reaching out. We'll be in touch soon!" } +end+ |
+
+ + + +5 +6 +7 +8 +9+ |
+
+ # File 'app/mailers/wcc/contentful/app/contact_mailer.rb', line 5 + +def contact_form_email(to_email, data) + @form_data = data + + mail(from: @form_data[:Email], to: to_email, subject: "#{@form_data[:internal_title]} Submission") +end+ |
+
A new instance of CustomMarkdownRender.
+Returns a new instance of CustomMarkdownRender.
+ + +
+ + + +7 +8 +9 +10+ |
+
+ # File 'lib/wcc/contentful/app/custom_markdown_render.rb', line 7 + +def initialize() + super + @options = +end+ |
+
+ + + +27 +28 +29 +30 +31 +32 +33 +34 +35+ |
+
+ # File 'lib/wcc/contentful/app/custom_markdown_render.rb', line 27 + +def hyperlink_attributes(title, url, link_class = nil) + link_attrs = { title: title, class: link_class } + + link_attrs[:target] = use_target_blank?(url) ? '_blank' : nil + + return link_attrs unless @options[:link_attributes] + + @options[:link_attributes].merge(link_attrs) +end+ |
+
+ + + +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25+ |
+
+ # File 'lib/wcc/contentful/app/custom_markdown_render.rb', line 12 + +def link(link, title, content) + link_with_class_data = + @options[:links_with_classes]&.find do |link_with_class| + link_with_class[0] == link && + link_with_class[2] == CGI.unescape_html(content) + end + + link_class = link_with_class_data ? link_with_class_data[3] : nil + ActionController::Base.helpers.link_to( + content, + link, + hyperlink_attributes(title, link, link_class) + ) +end+ |
+
+ + + +41 +42 +43+ |
+
+ # File 'lib/wcc/contentful/app/custom_markdown_render.rb', line 41 + +def table(header, body) + "<table class=\"table\">#{header}#{body}</table>" +end+ |
+
+ + + +37 +38 +39+ |
+
+ # File 'lib/wcc/contentful/app/custom_markdown_render.rb', line 37 + +def use_target_blank?(url) + url.scan(/(\s|^)(https?:\/\/\S*)/).present? +end+ |
+
Returns the value of attribute extensions.
+Returns the value of attribute options.
+A new instance of MarkdownRenderer.
+Returns a new instance of MarkdownRenderer.
+ + +
+ + + +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25+ |
+
+ # File 'lib/wcc/contentful/app/markdown_renderer.rb', line 8 + +def initialize( = nil) + = &.dup + + @extensions = { + autolink: true, + superscript: true, + disable_indented_code_blocks: true, + tables: true + }.merge!(&.delete(:extensions) || {}) + + @options = { + filter_html: true, + hard_wrap: true, + link_attributes: { target: '_blank' }, + space_after_headers: true, + fenced_code_blocks: true + }.merge!( || {}) +end+ |
+
Returns the value of attribute extensions.
+ + +
+ + + +6 +7 +8+ |
+
+ # File 'lib/wcc/contentful/app/markdown_renderer.rb', line 6 + +def extensions + @extensions +end+ |
+
Returns the value of attribute options.
+ + +
+ + + +6 +7 +8+ |
+
+ # File 'lib/wcc/contentful/app/markdown_renderer.rb', line 6 + +def + @options +end+ |
+
+ + + +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40+ |
+
+ # File 'lib/wcc/contentful/app/markdown_renderer.rb', line 27 + +def markdown(text) + raise ArgumentError, 'markdown method requires text' unless text + + markdown_links = links_within_markdown(text) + links_with_classes, raw_classes = gather_links_with_classes_data(markdown_links) + + = @options.merge({ + links_with_classes: links_with_classes + }) + + renderer = ::WCC::Contentful::App::CustomMarkdownRender.new() + markdown = ::Redcarpet::Markdown.new(renderer, extensions) + markdown.render(remove_markdown_href_class_syntax(raw_classes, text)) +end+ |
+
An href is local if it points to a part of the page.
+
+ + + +4 +5 +6+ |
+
+ # File 'app/helpers/wcc/contentful/app/menu_helper.rb', line 4 + +def dropdown?(item) + item.respond_to?(:items) +end+ |
+
+ + + +70 +71 +72 +73+ |
+
+ # File 'app/helpers/wcc/contentful/app/menu_helper.rb', line 70 + +def hash_only(href) + url = URI(href) + "##{url.fragment}" if url.fragment.present? +end+ |
+
+ + + +12 +13 +14 +15 +16 +17 +18+ |
+
+ # File 'app/helpers/wcc/contentful/app/menu_helper.rb', line 12 + +def item_active?(item) + return true if item.try(:label) && item_active?(item.label) + return item.items.any? { |i| item_active?(i) } if item.respond_to?(:items) + return current_page?(item.href) if item.try(:href) + + false +end+ |
+
An href is local if it points to a part of the page
+ + +
+ + + +76 +77 +78 +79 +80 +81 +82 +83 +84+ |
+
+ # File 'app/helpers/wcc/contentful/app/menu_helper.rb', line 76 + +def local?(href) + return true if href =~ /^#/ + + url = URI(href) + return false unless url.fragment.present? + + url.fragment = nil + current_page?(url.to_s) +end+ |
+
+ + + +8 +9 +10+ |
+
+ # File 'app/helpers/wcc/contentful/app/menu_helper.rb', line 8 + +def (item) + item.is_a? WCC::Contentful::Model::MenuButton +end+ |
+
+ + + +66 +67 +68+ |
+
+ # File 'app/helpers/wcc/contentful/app/menu_helper.rb', line 66 + +def push_class(classes, ) + [:class] = [*classes, *[:class]] +end+ |
+
+ + + +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40+ |
+
+ # File 'app/helpers/wcc/contentful/app/menu_helper.rb', line 20 + +def (, = {}, &block) + html = (, , &block) + + if .try(:external?) + push_class('external', ) + [:target] = :_blank + end + if .icon.present? || .material_icon.present? || .dig(:icon, :fallback) + push_class('icon-only', ) unless .text.present? + elsif .text.present? + push_class('text-only', ) + end + + push_class(.style, ) if .style + + href = .href + href = hash_only(href) if href.present? && local?(href) + return link_to(html, href, ) if href.present? + + content_tag(:a, html, ) +end+ |
+
+ + + +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60+ |
+
+ # File 'app/helpers/wcc/contentful/app/menu_helper.rb', line 50 + +def (icon, = {}) + fallback = &.delete(:fallback) + return fallback&.call unless icon + + = { + alt: icon.description || icon.title, + width: icon.file.dig('details', 'image', 'width'), + height: icon.file.dig('details', 'image', 'height') + }.merge!( || {}) + image_tag(icon&.file&.url, ) +end+ |
+
+ + + +42 +43 +44 +45 +46 +47 +48+ |
+
+ # File 'app/helpers/wcc/contentful/app/menu_helper.rb', line 42 + +def (, = {}, &block) + html = (.icon, .delete(:icon)) || ''.html_safe + html += (.material_icon) + content_tag(:span, .text) + + html += capture(&block) if block_given? + html +end+ |
+
+ + + +62 +63 +64+ |
+
+ # File 'app/helpers/wcc/contentful/app/menu_helper.rb', line 62 + +def (material_icon) + content_tag(:i, material_icon&.downcase, class: ['material-icons']) +end+ |
+
Returns the value of attribute slug.
+A new instance of PageNotFoundError.
+Returns a new instance of PageNotFoundError.
+ + +
+ + + +40 +41 +42 +43+ |
+
+ # File 'lib/wcc/contentful/app/exceptions.rb', line 40 + +def initialize(slug) + super("Page not found: '#{slug}'") + @slug = slug +end+ |
+
Returns the value of attribute slug.
+ + +
+ + + +38 +39 +40+ |
+
+ # File 'lib/wcc/contentful/app/exceptions.rb', line 38 + +def slug + @slug +end+ |
+
+ + + +8 +9 +10 +11 +12+ |
+
+ # File 'app/controllers/wcc/contentful/app/pages_controller.rb', line 8 + +def index + @page = global_site_config&.homepage || + page_model.find_by(slug: '/', options: { include: 3, preview: preview? }) + render 'pages/show' +end+ |
+
+ + + +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24+ |
+
+ # File 'app/controllers/wcc/contentful/app/pages_controller.rb', line 14 + +def show + slug = "/#{params[:slug]}" + @page = page_model.find_by(slug: slug, options: { include: 3, preview: preview? }) + + return render 'pages/show' if @page + + redirect = redirect_model.find_by(slug: slug, options: { include: 0, preview: preview? }) + raise WCC::Contentful::App::PageNotFoundError, slug unless redirect + + redirect_to redirect.href +end+ |
+
+ + + +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14+ |
+
+ # File 'app/models/concerns/wcc/contentful/app/preview_password.rb', line 4 + +def preview? + # check ApplicationController for a :preview? method + return super if defined?(super) + + @preview ||= + if preview_password.present? + params[:preview]&.chomp == preview_password.chomp + else + false + end +end+ |
+
+ + + +16 +17 +18+ |
+
+ # File 'app/models/concerns/wcc/contentful/app/preview_password.rb', line 16 + +def preview_password + WCC::Contentful::App.configuration.preview_password +end+ |
+
+ + + +37 +38 +39 +40 +41 +42 +43 +44 +45 +46+ |
+
+ # File 'app/helpers/wcc/contentful/app/section_helper.rb', line 37 + +def markdown(text, = {}) + renderer = WCC::Contentful::App::MarkdownRenderer.new( + + ) + html_to_render = renderer.markdown(text) + + content_tag(:div, + CGI.unescapeHTML(html_to_render).html_safe, + class: 'formatted-content') +end+ |
+
+ + + +8 +9 +10+ |
+
+ # File 'app/helpers/wcc/contentful/app/section_helper.rb', line 8 + +def render_section(section, index) + render('components/section', section: section, index: index) +end+ |
+
+ + + +48 +49 +50 +51 +52 +53 +54 +55 +56 +57+ |
+
+ # File 'app/helpers/wcc/contentful/app/section_helper.rb', line 48 + +def safe_line_break(text, = {}) + return unless text.present? + + text = CGI.escapeHTML(text) + text = text.gsub(/&(nbsp|vert|\#\d+);/, '&\1;') + .gsub(/<br\/?>/, '<br/>') + content_tag(:span, text.html_safe, { + class: 'safe-line-break' + }.merge()) +end+ |
+
+ + + +16 +17 +18+ |
+
+ # File 'app/helpers/wcc/contentful/app/section_helper.rb', line 16 + +def section_css_name(section) + section_template_name(section).dasherize +end+ |
+
+ + + +32 +33 +34 +35+ |
+
+ # File 'app/helpers/wcc/contentful/app/section_helper.rb', line 32 + +def section_id(section) + title = section.try(:bookmark_title) || section.try(:title) + CGI.escape(title.gsub(/\W+/, '-')) if title.present? +end+ |
+
+ + + +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30+ |
+
+ # File 'app/helpers/wcc/contentful/app/section_helper.rb', line 20 + +def section_styles(section) + section_styles = ["section-#{section_css_name(section)}"] + if styles = section.try(:styles) + section_styles.push(styles.map { |style| style.downcase.gsub(/[^\w]/, '-') }) + elsif style = section.try(:style) + section_styles.push(style.downcase.gsub(/[^\w]/, '-')) + else + section_styles.push('default') + end + section_styles +end+ |
+
+ + + +12 +13 +14+ |
+
+ # File 'app/helpers/wcc/contentful/app/section_helper.rb', line 12 + +def section_template_name(section) + section.class.name.demodulize.underscore.sub('section_', '') +end+ |
+
+ + + +59 +60 +61 +62 +63+ |
+
+ # File 'app/helpers/wcc/contentful/app/section_helper.rb', line 59 + +def split_content_for_mobile_view(visible_count, speakers) + visible_count = visible_count.to_i + speakers = [*speakers].compact + [speakers.shift(visible_count), speakers] +end+ |
+
Raised by WCC::Contentful.validate_models! if a content type in the space does not match the validation defined on the associated model.
+ + ++ + + + + Classes: Message + + +
+ + + + +Returns the value of attribute errors.
+Turns the error messages hash into an array of message structs like: menu.fields.name.type: must be equal to String.
+A new instance of ValidationError.
+Returns a new instance of ValidationError.
+ + +
+ + + +16 +17 +18 +19+ |
+
+ # File 'lib/wcc/contentful/app/exceptions.rb', line 16 + +def initialize(errors) + @errors = ValidationError.join_msg_keys(errors) + super("Content Type Schema from Contentful failed validation!\n #{@errors.join("\n ")}") +end+ |
+
Returns the value of attribute errors.
+ + +
+ + + +14 +15 +16+ |
+
+ # File 'lib/wcc/contentful/app/exceptions.rb', line 14 + +def errors + @errors +end+ |
+
Turns the error messages hash into an array of message structs like: menu.fields.name.type: must be equal to String
+ + +
+ + + +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34+ |
+
+ # File 'lib/wcc/contentful/app/exceptions.rb', line 23 + +def self.join_msg_keys(hash) + ret = + hash.map do |k, v| + if v.is_a?(Hash) + msgs = join_msg_keys(v) + msgs.map { |msg| Message.new("#{k}.#{msg.path}", msg.error) } + else + v.map { |msg| Message.new(k.to_s, msg) } + end + end + ret.flatten(1) +end+ |
+
Returns the value of attribute error.
+Returns the value of attribute path.
+Returns the value of attribute error
+ + +
+ + + +7 +8 +9+ |
+
+ # File 'lib/wcc/contentful/app/exceptions.rb', line 7 + +def error + @error +end+ |
+
Returns the value of attribute path
+ + +
+ + + +7 +8 +9+ |
+
+ # File 'lib/wcc/contentful/app/exceptions.rb', line 7 + +def path + @path +end+ |
+
+ + + +9 +10 +11+ |
+
+ # File 'lib/wcc/contentful/app/exceptions.rb', line 9 + +def to_s + "#{path}: #{error}" +end+ |
+
A menu link is external if `external_link` is present and not relative.
+Gets either the external link or the slug from the referenced page.
+A menu link is external if `external_link` is present and not relative.
+ + +
+ + + +9 +10 +11+ |
+
+ # File 'lib/wcc/contentful/model/menu_button.rb', line 9 + +def external? + external_uri&.scheme.present? +end+ |
+
+ + + +4 +5 +6+ |
+
+ # File 'lib/wcc/contentful/model/menu_button.rb', line 4 + +def external_uri + @external_url ||= URI(external_link) if external_link.present? +end+ |
+
+ + + +26 +27 +28+ |
+
+ # File 'lib/wcc/contentful/model/menu_button.rb', line 26 + +def fragment + WCC::Contentful::App::SectionHelper.section_id(section_link) if section_link +end+ |
+
Gets either the external link or the slug from the referenced page. Example usage: `<%= link_to button.title, button.href %>`
+ + +
+ + + +15 +16 +17 +18 +19 +20 +21 +22 +23 +24+ |
+
+ # File 'lib/wcc/contentful/model/menu_button.rb', line 15 + +def href + return external_link if external_link + + url = (link&.try(:slug) || link&.try(:url)) + return url unless fragment.present? + + url = URI(url || '') + url.fragment = fragment + url.to_s +end+ |
+
A menu link is external if `external_link` is present and not relative.
+Gets either the external link or the slug from the referenced page.
+A menu link is external if `external_link` is present and not relative.
+ + +
+ + + +9 +10 +11+ |
+
+ # File 'lib/wcc/contentful/model/redirect.rb', line 9 + +def external? + external_uri&.scheme.present? +end+ |
+
+ + + +4 +5 +6+ |
+
+ # File 'lib/wcc/contentful/model/redirect.rb', line 4 + +def external_uri + @external_uri ||= URI(external_link) if external_link.present? +end+ |
+
+ + + +26 +27 +28+ |
+
+ # File 'lib/wcc/contentful/model/redirect.rb', line 26 + +def fragment + WCC::Contentful::App::SectionHelper.section_id(section_link) if section_link +end+ |
+
Gets either the external link or the slug from the referenced page. Example usage: `redirect_to redirect.href`
+ + +
+ + + +15 +16 +17 +18 +19 +20 +21 +22 +23 +24+ |
+
+ # File 'lib/wcc/contentful/model/redirect.rb', line 15 + +def href + return external_link if external_link + + url = (link&.try(:slug) || link&.try(:url)) + return url unless fragment.present? + + url = URI(url || '') + url.fragment = fragment + url.to_s +end+ |
+
+ + + +10 +11 +12+ |
+
+ # File 'lib/wcc/contentful/model/section_contact_form.rb', line 10 + +def page + ::WCC::Contentful::Model::Page.find_by(sections: { id: id }) +end+ |
+
+ + + +4 +5 +6 +7 +8+ |
+
+ # File 'lib/wcc/contentful/model/section_contact_form.rb', line 4 + +def send_email(data) + save_contact_form(data) + + ::WCC::Contentful::App::ContactMailer.contact_form_email(data[:notification_email], data).deliver +end+ |
+
+ + + +14 +15 +16 +17 +18+ |
+
+ # File 'lib/wcc/contentful/model/section_contact_form.rb', line 14 + +def to_address(email_object_id: nil) + return email_address(email_model(email_object_id)) if email_object_id.present? + + notification_email +end+ |
+
rubocop:disable Style/OptionalBooleanParameter.
+rubocop:disable Style/OptionalBooleanParameter
+ + +
+ + + +4 +5 +6+ |
+
+ # File 'lib/wcc/contentful/model/site_config.rb', line 4 + +def self.instance(preview = false) # rubocop:disable Style/OptionalBooleanParameter + find_by(foreign_key: 'default', options: { include: 4, preview: preview }) +end+ |
+
Dir.glob("#{__dir__}/templates/*") +.select { |f| File.directory? f } +.map { |f| File.basename f } +.sort +.freeze
A new instance of ModelGenerator.
+Returns a new instance of ModelGenerator.
+ + +
+ + + +15 +16 +17 +18 +19 +20 +21+ |
+
+ # File 'lib/generators/wcc/model_generator.rb', line 15 + +def initialize(*) + super + + return if VALID_MODELS.include?(singular) + + raise ArgumentError, "Model must be one of #{VALID_MODELS.to_sentence}" +end+ |
+
+ + + +71 +72 +73 +74 +75 +76 +77 +78+ |
+
+ # File 'lib/generators/wcc/model_generator.rb', line 71 + +def create_model_migrations + copy_file "#{singular}/migrations/generated_add_#{plural}.ts", + "db/migrate/#{}01_generated_add_#{plural}.ts" + + Dir.glob("#{__dir__}/templates/#{singular}/migrations/*.rb").each do |f| + copy_file f, "db/migrate/#{}_#{File.basename(f)}" + end +end+ |
+
+ + + +80 +81 +82+ |
+
+ # File 'lib/generators/wcc/model_generator.rb', line 80 + +def drop_model_overrides_in_app_models + directory "#{singular}/models", 'app/models' +end+ |
+
+ + + +65 +66 +67 +68 +69+ |
+
+ # File 'lib/generators/wcc/model_generator.rb', line 65 + +def ensure_initializer_exists + return if inside('config/initializers') { File.exist?('wcc_contentful.rb') } + + copy_file 'wcc_contentful.rb', 'config/initializers/wcc_contentful.rb' +end+ |
+
+ + + +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34+ |
+
+ # File 'lib/generators/wcc/model_generator.rb', line 23 + +def ensure_migration_tools_installed + in_root do + run 'npm init -y' unless File.exist?('package.json') + package = JSON.parse(File.read('package.json')) + deps = package['dependencies'] + + unless deps.try(:[], '@watermarkchurch/contentful-migration').present? + run 'npm install --save @watermarkchurch/contentful-migration ts-node ' \ + 'typescript contentful-export' + end + end +end+ |
+
+ + + +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63+ |
+
+ # File 'lib/generators/wcc/model_generator.rb', line 36 + +def ensure_wrapper_script_in_bin_dir + copy_file 'contentful_shell_wrapper', 'bin/contentful' unless inside('bin') { File.exist?('contentful') } + + if inside('bin') { File.exist?('release') } + release = inside('bin') { File.read('release') } + unless release.include?('contentful migrate') + insert_into_file('bin/release', after: 'bundle exec rake db:migrate') do + <<~HEREDOC + DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + $DIR/contentful migrate -y + HEREDOC + end + end + else + copy_file 'release', 'bin/release' + end + + if in_root { File.exist?('Procfile') } + procfile = in_root { File.read('Procfile') } + unless procfile.include?('release:') + insert_into_file('Procfile') do + 'release: bin/release' + end + end + else + copy_file 'Procfile' + end +end+ |
+
+ + + + + Classes: ModelGenerator + + +
+ + + + + + + + + +
+
+
+
|
+
+
+
+
+
+
|
+
t |
+ + + +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46+ |
+
+ # File 'lib/wcc/contentful/middleman/extension.rb', line 20 + +def initialize(app, = {}, &block) + # don't pass block to super b/c we use it to configure WCC::Contentful + super(app, ) {} # rubocop:disable Lint/EmptyBlock + + # Require libraries only when activated + require 'wcc/contentful' + + # set up your extension + return if WCC::Contentful.initialized + + WCC::Contentful.configure do |config| + config.store :eager_sync, :memory + + .to_h.each do |(k, v)| + config.public_send("#{k}=", v) if config.respond_to?("#{k}=") + end + + instance_exec(config, &block) if block_given? + end + + WCC::Contentful.init! + model_glob = File.join(Middleman::Application.root, 'lib/models/**/*.rb') + Dir[model_glob].sort.each { |f| require f } + + # Sync the latest data from Contentful + WCC::Contentful::Services.instance.sync_engine&.next +end+ |
+
helpers do
+ +def a_helper
+end
+
+
+end
+ + +
+ + + +53 +54 +55 +56+ |
+
+ # File 'lib/wcc/contentful/middleman/extension.rb', line 53 + +def ready + # resync every page load in development & test mode only + app.use ContentfulSyncUpdate if app.server? +end+ |
+
Rack app that advances the sync engine whenever we load a page
+ + +A new instance of ContentfulSyncUpdate.
+Returns a new instance of ContentfulSyncUpdate.
+ + +
+ + + +60 +61 +62+ |
+
+ # File 'lib/wcc/contentful/middleman/extension.rb', line 60 + +def initialize(app) + @app = app +end+ |
+
+ + + +74 +75 +76+ |
+
+ # File 'lib/wcc/contentful/middleman/extension.rb', line 74 + +def last_sync + @@last_sync ||= Time.at(0) # rubocop:disable Style/ClassVars +end+ |
+
+ + + +78 +79 +80+ |
+
+ # File 'lib/wcc/contentful/middleman/extension.rb', line 78 + +def last_sync=(time) + @@last_sync = time # rubocop:disable Style/ClassVars +end+ |
+
+ + + +64 +65 +66 +67 +68 +69 +70 +71+ |
+
+ # File 'lib/wcc/contentful/middleman/extension.rb', line 64 + +def call(env) + if (Time.now - ContentfulSyncUpdate.last_sync) > 10.seconds + ::WCC::Contentful::Services.instance.sync_engine&.next + ContentfulSyncUpdate.last_sync = Time.now + end + + @app.call(env) +end+ |
+
+
+
+
|
+
t |
+ + + +35 +36 +37+ |
+
+ # File 'lib/wcc/contentful.rb', line 35 + +def configuration + @configuration +end+ |
+
Returns the value of attribute initialized.
+ + +
+ + + +32 +33 +34+ |
+
+ # File 'lib/wcc/contentful.rb', line 32 + +def initialized + @initialized +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. See WCC::Contentful::Configuration for all configuration options.
+ + +
+ + + +58 +59 +60 +61 +62 +63 +64 +65 +66 +67+ |
+
+ # File 'lib/wcc/contentful.rb', line 58 + +def self.configure + raise InitializationError, 'Cannot configure after initialization' if @initialized + + @configuration ||= Configuration.new + yield(configuration) + + configuration.validate! + + 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')
+
+
+
+
+ + + +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135+ |
+
+ # File 'lib/wcc/contentful.rb', line 78 + +def self.init! + raise InitializationError, 'Please first call WCC:Contentful.configure' if configuration.nil? + raise InitializationError, 'Already Initialized' if @initialized + + if configuration.update_schema_file == :always || + (configuration.update_schema_file == :if_possible && Services.instance.management_client) || + (configuration.update_schema_file == :if_missing && !File.exist?(configuration.schema_file)) + + begin + downloader = WCC::Contentful::DownloadsSchema.new + downloader.update! if configuration.update_schema_file == :always || downloader.needs_update? + rescue WCC::Contentful::SimpleClient::ApiError => e + raise InitializationError, e if configuration.update_schema_file == :always + + Services.instance.logger.warn("Unable to download schema from management API - #{e.}") + end + end + + content_types = + begin + JSON.parse(File.read(configuration.schema_file))['contentTypes'] if File.exist?(configuration.schema_file) + rescue JSON::ParserError + Services.instance.warn("Schema file invalid, ignoring it: #{configuration.schema_file}") + nil + end + + if !content_types && %i[if_possible never].include?(configuration.update_schema_file) + # Final fallback - try to grab content types from CDN. We can't update the file + # because the CDN doesn't have all the field validation info, but we can at least + # build the WCC::Contentful::Model instances. + client = Services.instance.management_client || + Services.instance.client + begin + content_types = client.content_types(limit: 1000).items if client + rescue WCC::Contentful::SimpleClient::ApiError => e + # indicates bad credentials + Services.instance.logger.warn("Unable to load content types from API - #{e.}") + end + end + + unless content_types + raise InitializationError, 'Unable to load content types from schema file or API! ' \ + 'Check your access token and space ID.' + end + + # Set the schema on the default WCC::Contentful::Model + WCC::Contentful::Model.configure( + configuration, + schema: WCC::Contentful::ContentTypeIndexer.from_json_schema(content_types).types, + services: WCC::Contentful::Services.instance + ) + + # Drop an initial sync + WCC::Contentful::SyncEngine::Job.perform_later if defined?(WCC::Contentful::SyncEngine::Job) + + @configuration = @configuration.freeze + @initialized = true +end+ |
+
Gets all queryable locales. Reserved for future use.
+ + +
+ + + +44 +45 +46+ |
+
+ # File 'lib/wcc/contentful.rb', line 44 + +def locales + @locales ||= { 'en-US' => {} }.freeze +end+ |
+
+ + + +48 +49 +50 +51+ |
+
+ # File 'lib/wcc/contentful.rb', line 48 + +def logger + ActiveSupport::Deprecation.warn('Use WCC::Contentful::Services.instance.logger instead') + WCC::Contentful::Services.instance.logger +end+ |
+
+ + + +37 +38 +39 +40+ |
+
+ # File 'lib/wcc/contentful.rb', line 37 + +def types + ActiveSupport::Deprecation.warn('Use WCC::Contentful::Model.schema instead') + WCC::Contentful::Model.schema +end+ |
+
+ + + +6 +7 +8+ |
+
+ # File 'lib/wcc/contentful/active_record_shim.rb', line 6 + +def attributes + @attributes ||= to_h['fields'].tap { |fields| fields['id'] = id } +end+ |
+
+ + + +10 +11 +12 +13 +14+ |
+
+ # File 'lib/wcc/contentful/active_record_shim.rb', line 10 + +def cache_key + return cache_key_with_version unless ActiveRecord::Base.try(:cache_versioning) == true + + "#{self.class.model_name}/#{id}" +end+ |
+
+ + + +16 +17 +18+ |
+
+ # File 'lib/wcc/contentful/active_record_shim.rb', line 16 + +def cache_key_with_version + "#{cache_key_without_version}-#{cache_version}" +end+ |
+
+ + + +20 +21 +22+ |
+
+ # File 'lib/wcc/contentful/active_record_shim.rb', line 20 + +def cache_key_without_version + "#{self.class.model_name}/#{id}" +end+ |
+
+ + + +24 +25 +26+ |
+
+ # File 'lib/wcc/contentful/active_record_shim.rb', line 24 + +def cache_version + sys.revision.to_s +end+ |
+
Raised when an entry contains a circular reference and cannot be represented as a flat tree.
+ + +Returns the value of attribute id.
+Returns the value of attribute stack.
+A new instance of CircularReferenceError.
+Returns a new instance of CircularReferenceError.
+ + +
+ + + +17 +18 +19 +20 +21+ |
+
+ # File 'lib/wcc/contentful/exceptions.rb', line 17 + +def initialize(stack, id) + @id = id + @stack = stack.slice(stack.index(id)..stack.length) + super('Circular reference detected!') +end+ |
+
Returns the value of attribute id.
+ + +
+ + + +15 +16 +17+ |
+
+ # File 'lib/wcc/contentful/exceptions.rb', line 15 + +def id + @id +end+ |
+
Returns the value of attribute stack.
+ + +
+ + + +15 +16 +17+ |
+
+ # File 'lib/wcc/contentful/exceptions.rb', line 15 + +def stack + @stack +end+ |
+
+ + + +23 +24 +25 +26 +27 +28 +29+ |
+
+ # File 'lib/wcc/contentful/exceptions.rb', line 23 + +def + return super unless stack + + super + "\n " \ + "#{stack.last} points to #{id} which is also it's ancestor\n " + + stack.join('->') +end+ |
+
This object contains all the configuration options for the `wcc-contentful` gem.
+ + ++ + + + + Classes: FrozenConfiguration + + +
+ + +%i[ + access_token + app_url + connection + connection_options + default_locale + environment + instrumentation_adapter + logger + management_token + preview_token + schema_file + space + store + sync_retry_limit + sync_retry_wait + update_schema_file + webhook_jobs + webhook_password + webhook_username +].freeze
(required) Sets the Content Delivery API access token.
+Sets the app's root URL for a Rails app.
+Sets the connection which is used to make HTTP requests.
+Sets the connection options which are given to the client.
+Sets the default locale.
+Sets the Environment ID.
+Overrides the use of ActiveSupport::Notifications throughout this library to emit instrumentation events.
+Sets the logger to be used by the wcc-contentful gem, including stores.
+Sets the Content Management Token used to communicate with the Management API.
+Sets the Content Preview API access token.
+(required) Sets the Contentful Space ID.
+Explicitly read the store factory.
+Sets the maximum number of times that the SyncEngine will retry synchronization when it detects that the Contentful CDN's cache has not been updated after a webhook.
+Sets the base ActiveSupport::Duration that the SyncEngine will wait before retrying.
+Returns the value of attribute update_schema_file.
+An array of jobs that are run whenever a webhook is received by the webhook controller.
+Sets an optional basic auth password that will be validated by the webhook controller.
+Sets an optional basic auth username that will be validated by the webhook controller.
+A new instance of Configuration.
+Returns true if the currently configured environment is pointing at `master`.
+Defines the method by which content is downloaded from the Contentful CDN.
+Convenience for setting store without a block.
+Validates the configuration, raising ArgumentError if anything is wrong.
+Returns a new instance of Configuration.
+ + +
+ + + +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210+ |
+
+ # File 'lib/wcc/contentful/configuration.rb', line 191 + +def initialize + @access_token = ENV.fetch('CONTENTFUL_ACCESS_TOKEN', nil) + @app_url = ENV.fetch('APP_URL', nil) + @connection_options = { + api_url: 'https://cdn.contentful.com/', + preview_api_url: 'https://preview.contentful.com/', + management_api_url: 'https://api.contentful.com' + } + @management_token = ENV.fetch('CONTENTFUL_MANAGEMENT_TOKEN', nil) + @preview_token = ENV.fetch('CONTENTFUL_PREVIEW_TOKEN', nil) + @space = ENV.fetch('CONTENTFUL_SPACE_ID', nil) + @default_locale = nil + @middleware = [] + @update_schema_file = :if_possible + @schema_file = 'db/contentful-schema.json' + @webhook_jobs = [] + @store_factory = WCC::Contentful::Store::Factory.new(self, :direct) + @sync_retry_limit = 3 + @sync_retry_wait = 1.second +end+ |
+
(required) Sets the Content Delivery API access token.
+ + +
+ + + +30 +31 +32+ |
+
+ # File 'lib/wcc/contentful/configuration.rb', line 30 + +def access_token + @access_token +end+ |
+
Sets the app's root URL for a Rails app. Used by the WCC::Contentful::Engine to automatically set up webhooks to point at the WCC::Contentful::WebhookController
+ + +
+ + + +34 +35 +36+ |
+
+ # File 'lib/wcc/contentful/configuration.rb', line 34 + +def app_url + @app_url +end+ |
+
Sets the connection which is used to make HTTP requests. If left unset, the gem attempts to load 'faraday' or 'typhoeus'. You can pass your own adapter which responds to 'get' and 'post', and returns a response that quacks like Faraday.
+ + +
+ + + +134 +135 +136+ |
+
+ # File 'lib/wcc/contentful/configuration.rb', line 134 + +def connection + @connection +end+ |
+
Sets the connection options which are given to the client. This can include an alternative Cdn API URL, timeouts, etc. See WCC::Contentful::SimpleClient constructor for details.
+ + +
+ + + +139 +140 +141+ |
+
+ # File 'lib/wcc/contentful/configuration.rb', line 139 + +def + @connection_options +end+ |
+
Sets the default locale. Defaults to 'en-US'.
+ + +
+ + + +42 +43 +44+ |
+
+ # File 'lib/wcc/contentful/configuration.rb', line 42 + +def default_locale + @default_locale +end+ |
+
Sets the Environment ID. Leave blank to use master.
+ + +
+ + + +40 +41 +42+ |
+
+ # File 'lib/wcc/contentful/configuration.rb', line 40 + +def environment + @environment +end+ |
+
Overrides the use of ActiveSupport::Notifications throughout this library to emit instrumentation events. The object or module provided here must respond to :instrument like ActiveSupport::Notifications.instrument
+ + +
+ + + +184 +185 +186+ |
+
+ # File 'lib/wcc/contentful/configuration.rb', line 184 + +def instrumentation_adapter + @instrumentation_adapter +end+ |
+
Sets the logger to be used by the wcc-contentful gem, including stores. Defaults to the rails logger if in a rails context, otherwise creates a new logger that writes to STDERR.
+ + +
+ + + +189 +190 +191+ |
+
+ # File 'lib/wcc/contentful/configuration.rb', line 189 + +def logger + @logger +end+ |
+
Sets the Content Management Token used to communicate with the Management API. This is required for automatically setting up webhooks, and to create the WCC::Contentful::Services#management_client.
+ + +
+ + + +38 +39 +40+ |
+
+ # File 'lib/wcc/contentful/configuration.rb', line 38 + +def management_token + @management_token +end+ |
+
Sets the Content Preview API access token. Only required if you use the preview flag.
+ + +
+ + + +45 +46 +47+ |
+
+ # File 'lib/wcc/contentful/configuration.rb', line 45 + +def preview_token + @preview_token +end+ |
+
+ + + +173 +174 +175 +176 +177 +178 +179+ |
+
+ # File 'lib/wcc/contentful/configuration.rb', line 173 + +def schema_file + if defined?(Rails) + Rails.root.join(@schema_file) + else + @schema_file + end +end+ |
+
(required) Sets the Contentful Space ID.
+ + +
+ + + +28 +29 +30+ |
+
+ # File 'lib/wcc/contentful/configuration.rb', line 28 + +def space + @space +end+ |
+
Explicitly read the store factory
+ + +
+ + + +128 +129 +130+ |
+
+ # File 'lib/wcc/contentful/configuration.rb', line 128 + +def store_factory + @store_factory +end+ |
+
Sets the maximum number of times that the SyncEngine will retry synchronization when it detects that the Contentful CDN's cache has not been updated after a webhook. Default: 2
+ + +
+ + + +66 +67 +68+ |
+
+ # File 'lib/wcc/contentful/configuration.rb', line 66 + +def sync_retry_limit + @sync_retry_limit +end+ |
+
Sets the base ActiveSupport::Duration that the SyncEngine will wait before retrying. Each subsequent retry uses an exponential backoff, so the second retry will be after (2 * sync_retry_wait), the third after (4 * sync_retry_wait), etc. Default: 2.seconds
+ + +
+ + + +72 +73 +74+ |
+
+ # File 'lib/wcc/contentful/configuration.rb', line 72 + +def sync_retry_wait + @sync_retry_wait +end+ |
+
Returns the value of attribute update_schema_file.
+ + +
+ + + +166 +167 +168+ |
+
+ # File 'lib/wcc/contentful/configuration.rb', line 166 + +def update_schema_file + @update_schema_file +end+ |
+
An array of jobs that are run whenever a webhook is received by the webhook controller. The job can be an ActiveJob class which responds to `:perform_later`, or a lambda or other object that responds to `:call`. Example:
+ +config.webhook_jobs << MyJobClass
+config.webhook_jobs << ->(event) { ... }
+
+
+See the source code for WCC::Contentful::SyncEngine::Job for an example of how to implement a webhook job.
+ + +
+ + + +61 +62 +63+ |
+
+ # File 'lib/wcc/contentful/configuration.rb', line 61 + +def webhook_jobs + @webhook_jobs +end+ |
+
Sets an optional basic auth password that will be validated by the webhook controller. You must ensure the configured webhook sets the “HTTP Basic Auth password”
+ + +
+ + + +51 +52 +53+ |
+
+ # File 'lib/wcc/contentful/configuration.rb', line 51 + +def webhook_password + @webhook_password +end+ |
+
Sets an optional basic auth username that will be validated by the webhook controller. You must ensure the configured webhook sets the “HTTP Basic Auth username”
+ + +
+ + + +48 +49 +50+ |
+
+ # File 'lib/wcc/contentful/configuration.rb', line 48 + +def webhook_username + @webhook_username +end+ |
+
+ + + +235 +236 +237+ |
+
+ # File 'lib/wcc/contentful/configuration.rb', line 235 + +def freeze + FrozenConfiguration.new(self) +end+ |
+
+ + + +231 +232 +233+ |
+
+ # File 'lib/wcc/contentful/configuration.rb', line 231 + +def frozen? + false +end+ |
+
Returns true if the currently configured environment is pointing at `master`.
+ + +
+ + + +75 +76 +77+ |
+
+ # File 'lib/wcc/contentful/configuration.rb', line 75 + +def master? + !environment.present? +end+ |
+
Defines the method by which content is downloaded from the Contentful CDN.
+`config.store :direct` with the `:direct` method, all queries result in web requests to 'cdn.contentful.com' via the SimpleClient
+`config.store :eager_sync, [sync_store], [options]` with the `:eager_sync` method, the entire content of the Contentful space is downloaded locally and stored in the configured store. The application is responsible to periodically call the WCC::Contentful::SyncEngine#next to keep the store updated. Alternatively, the provided Engine can be mounted to automatically call WCC::Contentful::SyncEngine#next on webhook events. In `routes.rb` add the following:
+ +mount WCC::Contentful::Engine, at: '/'
+
+`config.store :lazy_sync, [cache]` The `:lazy_sync` method is a hybrid between the other two methods. Frequently accessed data is stored in an ActiveSupport::Cache implementation and is kept up-to-date via the Sync API. Any data that is not present in the cache is fetched from the CDN like in the `:direct` method. The application is still responsible to periodically call `sync!` or to mount the provided Engine.
+`config.store :custom, do … end` The block is executed in the context of a WCC::Contentful::Store::Factory. this can be used to apply middleware, etc.
+
+ + + +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120+ |
+
+ # File 'lib/wcc/contentful/configuration.rb', line 108 + +def store(*params, &block) + type, *params = params + if type + @store_factory = WCC::Contentful::Store::Factory.new( + self, + type, + params + ) + end + + @store_factory.instance_exec(&block) if block_given? + @store_factory +end+ |
+
Convenience for setting store without a block
+ + +
+ + + +123 +124 +125+ |
+
+ # File 'lib/wcc/contentful/configuration.rb', line 123 + +def store=(param_array) + store(*param_array) +end+ |
+
Validates the configuration, raising ArgumentError if anything is wrong. This is called by WCC::Contentful.init!
+ + +
+ + + +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229+ |
+
+ # File 'lib/wcc/contentful/configuration.rb', line 214 + +def validate! + raise ArgumentError, 'Please provide "space"' unless space.present? + raise ArgumentError, 'Please provide "access_token"' unless access_token.present? + + store_factory.validate! + + if update_schema_file == :always && management_token.blank? + raise ArgumentError, 'A management_token is required in order to update the schema file.' + end + + webhook_jobs.each do |job| + next if job.respond_to?(:call) || job.respond_to?(:perform_later) + + raise ArgumentError, "The job '#{job}' must be an instance of ActiveJob::Base or respond to :call" + end +end+ |
+
A new instance of FrozenConfiguration.
+Returns true if the currently configured environment is pointing at `master`.
+Returns a new instance of FrozenConfiguration.
+ + +
+ + + +242 +243 +244 +245 +246 +247 +248+ |
+
+ # File 'lib/wcc/contentful/configuration.rb', line 242 + +def initialize(configuration) + ATTRIBUTES.each do |att| + val = configuration.public_send(att) + val.freeze if val.is_a?(Hash) || val.is_a?(Array) + instance_variable_set("@#{att}", val) + end +end+ |
+
+ + + +255 +256 +257+ |
+
+ # File 'lib/wcc/contentful/configuration.rb', line 255 + +def frozen? + true +end+ |
+
Returns true if the currently configured environment is pointing at `master`.
+ + +
+ + + +251 +252 +253+ |
+
+ # File 'lib/wcc/contentful/configuration.rb', line 251 + +def master? + !environment.present? +end+ |
+
Returns the value of attribute types.
+hardcoded because the Asset type is a “magic type” in their system.
+A new instance of ContentTypeIndexer.
+#constant_from_content_type, #content_type_from_constant, #content_type_from_raw, #shared_prefix
+Returns a new instance of ContentTypeIndexer.
+ + +
+ + + +25 +26 +27 +28 +29+ |
+
+ # File 'lib/wcc/contentful/content_type_indexer.rb', line 25 + +def initialize + @types = IndexedRepresentation.new({ + 'Asset' => create_asset_type + }) +end+ |
+
Returns the value of attribute types.
+ + +
+ + + +23 +24 +25+ |
+
+ # File 'lib/wcc/contentful/content_type_indexer.rb', line 23 + +def types + @types +end+ |
+
+ + + +16 +17 +18 +19 +20+ |
+
+ # File 'lib/wcc/contentful/content_type_indexer.rb', line 16 + +def from_json_schema(schema) + new.tap do |ixr| + schema.each { |type| ixr.index(type) } + end +end+ |
+
+ + + +10 +11 +12 +13 +14+ |
+
+ # File 'lib/wcc/contentful/content_type_indexer.rb', line 10 + +def load(schema_file) + from_json_schema( + JSON.parse(File.read(schema_file))['contentTypes'] + ) +end+ |
+
hardcoded because the Asset type is a “magic type” in their system
+ + +
+ + + +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67+ |
+
+ # File 'lib/wcc/contentful/content_type_indexer.rb', line 57 + +def create_asset_type + IndexedRepresentation::ContentType.new({ + name: 'Asset', + content_type: 'Asset', + fields: { + 'title' => { name: 'title', type: :String }, + 'description' => { name: 'description', type: :String }, + 'file' => { name: 'file', type: :Json } + } + }) +end+ |
+
+ + + +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54+ |
+
+ # File 'lib/wcc/contentful/content_type_indexer.rb', line 42 + +def create_type(content_type_id, fields) + content_type = IndexedRepresentation::ContentType.new({ + name: constant_from_content_type(content_type_id), + content_type: content_type_id + }) + + fields.each do |f| + field = create_field(f) + content_type.fields[field.name] = field + end + + content_type +end+ |
+
+ + + +31 +32 +33 +34 +35 +36 +37 +38 +39 +40+ |
+
+ # File 'lib/wcc/contentful/content_type_indexer.rb', line 31 + +def index(content_type) + content_type = + if content_type.respond_to?(:fields) + create_type(content_type.id, content_type.fields) + else + create_type(content_type.dig('sys', 'id'), content_type['fields']) + end + + @types[content_type.content_type] = content_type +end+ |
+
Raised when a constant under Model does not match to a content type in the configured Contentful space
+ + +A new instance of DownloadsSchema.
+Returns a new instance of DownloadsSchema.
+ + +
+ + + +10 +11 +12 +13 +14 +15+ |
+
+ # File 'lib/wcc/contentful/downloads_schema.rb', line 10 + +def initialize(file = nil, management_client: nil) + @client = management_client || WCC::Contentful::Services.instance.management_client + @file = file || WCC::Contentful.configuration&.schema_file + raise ArgumentError, 'Please configure your management token' unless @client + raise ArgumentError, 'Please pass filename or call WCC::Contentful.configure' unless @file +end+ |
+
+ + + +6 +7 +8+ |
+
+ # File 'lib/wcc/contentful/downloads_schema.rb', line 6 + +def self.call(file = nil, management_client: nil) + new(file, management_client: management_client).call +end+ |
+
+ + + +17 +18 +19 +20 +21+ |
+
+ # File 'lib/wcc/contentful/downloads_schema.rb', line 17 + +def call + return unless needs_update? + + update! +end+ |
+
+ + + +52 +53 +54 +55 +56 +57 +58+ |
+
+ # File 'lib/wcc/contentful/downloads_schema.rb', line 52 + +def content_types + @content_types ||= + @client.content_types(limit: 1000) + .items + .map { |ct| strip_sys(ct) } + .sort_by { |ct| ct.dig('sys', 'id') } +end+ |
+
+ + + +60 +61 +62 +63 +64 +65 +66+ |
+
+ # File 'lib/wcc/contentful/downloads_schema.rb', line 60 + +def editor_interfaces + @editor_interfaces ||= + content_types + .map { |ct| @client.editor_interface(ct.dig('sys', 'id')).raw } + .map { |i| sort_controls(strip_sys(i)) } + .sort_by { |i| i.dig('sys', 'contentType', 'sys', 'id') } +end+ |
+
+ + + +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50+ |
+
+ # File 'lib/wcc/contentful/downloads_schema.rb', line 32 + +def needs_update? + return true unless File.exist?(@file) + + contents = + begin + JSON.parse(File.read(@file)) + rescue JSON::ParserError + return true # rubocop:disable Lint/NoReturnInBeginEndBlocks + end + + existing_cts = contents['contentTypes'].sort_by { |ct| ct.dig('sys', 'id') } + return true unless content_types.count == existing_cts.count + return true unless deep_contains_all(content_types, existing_cts) + + existing_eis = contents['editorInterfaces'].sort_by { |i| i.dig('sys', 'contentType', 'sys', 'id') } + return true unless editor_interfaces.count == existing_eis.count + + !deep_contains_all(editor_interfaces, existing_eis) +end+ |
+
+ + + +23 +24 +25 +26 +27 +28 +29 +30+ |
+
+ # File 'lib/wcc/contentful/downloads_schema.rb', line 23 + +def update! + FileUtils.mkdir_p(File.dirname(@file)) + + File.write(@file, format_json({ + 'contentTypes' => content_types, + 'editorInterfaces' => editor_interfaces + })) +end+ |
+
+ + + + + Classes: Asset, DeletedAsset, DeletedEntry, Entry, Registry, SyncComplete, Unknown + + +
+ + + + + + + + +Creates an Event out of a raw value received by a webhook or given from the Contentful Sync API.
+Creates an Event out of a raw value received by a webhook or given from the Contentful Sync API.
+ + +
+ + + +10 +11 +12 +13 +14+ |
+
+ # File 'lib/wcc/contentful/event.rb', line 10 + +def self.from_raw(raw, context = nil, source: nil) + const = Registry.instance.get(raw.dig('sys', 'type')) + + const.new(raw, context, source: source) +end+ |
+
+ + + +91 +92 +93+ |
+
+ # File 'lib/wcc/contentful/event.rb', line 91 + +def asset + @asset ||= WCC::Contentful::Model.new_from_raw(raw, sys.context) +end+ |
+
+ + + +87 +88 +89+ |
+
+ # File 'lib/wcc/contentful/event.rb', line 87 + +def content_type + 'Asset' +end+ |
+
+ + + +125 +126 +127+ |
+
+ # File 'lib/wcc/contentful/event.rb', line 125 + +def asset + @asset ||= WCC::Contentful::Model.new_from_raw(raw, sys.context) +end+ |
+
+ + + +121 +122 +123+ |
+
+ # File 'lib/wcc/contentful/event.rb', line 121 + +def content_type + 'Asset' +end+ |
+
+ + + +117 +118 +119+ |
+
+ # File 'lib/wcc/contentful/event.rb', line 117 + +def deleted_at + raw.dig('sys', 'deletedAt') +end+ |
+
+ + + +105 +106 +107+ |
+
+ # File 'lib/wcc/contentful/event.rb', line 105 + +def content_type + raw.dig('sys', 'contentType', 'sys', 'id') +end+ |
+
+ + + +101 +102 +103+ |
+
+ # File 'lib/wcc/contentful/event.rb', line 101 + +def deleted_at + raw.dig('sys', 'deletedAt') +end+ |
+
+ + + +109 +110 +111+ |
+
+ # File 'lib/wcc/contentful/event.rb', line 109 + +def entry + @entry ||= WCC::Contentful::Model.new_from_raw(raw, sys.context) +end+ |
+
+ + + +75 +76 +77+ |
+
+ # File 'lib/wcc/contentful/event.rb', line 75 + +def content_type + raw.dig('sys', 'contentType', 'sys', 'id') +end+ |
+
+ + + +79 +80 +81+ |
+
+ # File 'lib/wcc/contentful/event.rb', line 79 + +def entry + @entry ||= WCC::Contentful::Model.new_from_raw(raw, sys.context) +end+ |
+
+ + + +19 +20 +21 +22+ |
+
+ # File 'lib/wcc/contentful/event.rb', line 19 + +def get(name) + @event_types ||= {} + @event_types[name] || WCC::Contentful::Event::Unknown +end+ |
+
+ + + +24 +25 +26 +27 +28 +29 +30+ |
+
+ # File 'lib/wcc/contentful/event.rb', line 24 + +def register(constant) + name = constant.try(:type) || constant.name.demodulize + raise ArgumentError, "Constant #{constant} does not define 'new'" unless constant.respond_to?(:new) + + @event_types ||= {} + @event_types[name] = constant +end+ |
+
Returns the value of attribute items.
+Returns the value of attribute source.
+Returns the value of attribute sys.
+A new instance of SyncComplete.
+Returns a new instance of SyncComplete.
+ + +
+ + + +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148+ |
+
+ # File 'lib/wcc/contentful/event.rb', line 135 + +def initialize(items, context = nil, source: nil) + @items = items.freeze + @source = source + @sys = WCC::Contentful::Sys.new( + nil, + 'Array', + nil, + nil, + nil, + nil, + nil, + OpenStruct.new(context).freeze + ) +end+ |
+
Returns the value of attribute items.
+ + +
+ + + +150 +151 +152+ |
+
+ # File 'lib/wcc/contentful/event.rb', line 150 + +def items + @items +end+ |
+
Returns the value of attribute source.
+ + +
+ + + +150 +151 +152+ |
+
+ # File 'lib/wcc/contentful/event.rb', line 150 + +def source + @source +end+ |
+
Returns the value of attribute sys.
+ + +
+ + + +150 +151 +152+ |
+
+ # File 'lib/wcc/contentful/event.rb', line 150 + +def sys + @sys +end+ |
+
+ + + +152 +153 +154 +155 +156 +157 +158 +159+ |
+
+ # File 'lib/wcc/contentful/event.rb', line 152 + +def to_h + { + 'sys' => { + 'type' => 'Array' + }, + 'items' => items.map(&:to_h) + } +end+ |
+
WCC::Contentful::Events is a singleton which rebroadcasts Contentful update events. You can subscribe to these events in your initializer using the [wisper gem syntax](github.com/krisleech/wisper). All published events are in the namespace WCC::Contentful::Event.
+ + +A new instance of Events.
+Returns a new instance of Events.
+ + +
+ + + +17 +18 +19+ |
+
+ # File 'lib/wcc/contentful/events.rb', line 17 + +def initialize + _attach_listeners +end+ |
+
+ + + +13 +14 +15+ |
+
+ # File 'lib/wcc/contentful/events.rb', line 13 + +def self.instance + @instance ||= new +end+ |
+
+ + + +21 +22 +23 +24 +25 +26+ |
+
+ # File 'lib/wcc/contentful/events.rb', line 21 + +def rebroadcast(event) + type = event.dig('sys', 'type') + raise ArgumentError, "Unknown event type #{event}" unless type.present? + + broadcast(type, event) +end+ |
+
+ + + +17 +18 +19+ |
+
+ # File 'lib/wcc/contentful/helpers.rb', line 17 + +def constant_from_content_type(content_type) + content_type.gsub(/[^_a-zA-Z0-9]/, '_').camelize +end+ |
+
+ + + +28 +29 +30 +31 +32 +33+ |
+
+ # File 'lib/wcc/contentful/helpers.rb', line 28 + +def content_type_from_constant(const) + return const.content_type if const.respond_to?(:content_type) + + name = const.try(:name) || const.to_s + name.demodulize.camelize(:lower) +end+ |
+
+ + + +6 +7 +8 +9 +10 +11 +12 +13 +14 +15+ |
+
+ # File 'lib/wcc/contentful/helpers.rb', line 6 + +def content_type_from_raw(value) + case value.dig('sys', 'type') + when 'Entry', 'DeletedEntry' + value.dig('sys', 'contentType', 'sys', 'id') + when 'Asset', 'DeletedAsset' + 'Asset' + else + raise ArgumentError, "Unknown content type '#{value.dig('sys', 'type') || 'null'}'" + end +end+ |
+
+ + + +21 +22 +23 +24 +25 +26+ |
+
+ # File 'lib/wcc/contentful/helpers.rb', line 21 + +def shared_prefix(string_array) + string_array.reduce do |l, s| + l = l.chop while l != s[0...l.length] + l + end +end+ |
+
The result of running the indexer on raw content types to produce a type definition which can be used to build models or graphql types.
+ + ++ + + + + Classes: ContentType, Field + + +
+ + + + + + + + +A new instance of IndexedRepresentation.
+Returns a new instance of IndexedRepresentation.
+ + +
+ + + +7 +8 +9+ |
+
+ # File 'lib/wcc/contentful/indexed_representation.rb', line 7 + +def initialize(types = {}) + @types = types +end+ |
+
+ + + +22 +23 +24 +25 +26 +27 +28 +29 +30+ |
+
+ # File 'lib/wcc/contentful/indexed_representation.rb', line 22 + +def self.from_json(hash) + hash = JSON.parse(hash) if hash.is_a?(String) + + ret = IndexedRepresentation.new + hash.each do |id, content_type_hash| + ret[id] = ContentType.new(content_type_hash) + end + ret +end+ |
+
+ + + +40 +41 +42 +43 +44 +45+ |
+
+ # File 'lib/wcc/contentful/indexed_representation.rb', line 40 + +def ==(other) + my_keys = keys + return false unless my_keys == other.keys + + my_keys.all? { |k| self[k] == other[k] } +end+ |
+
+ + + +16 +17 +18 +19 +20+ |
+
+ # File 'lib/wcc/contentful/indexed_representation.rb', line 16 + +def []=(id, value) + raise ArgumentError unless value.is_a?(ContentType) + + @types[id] = value +end+ |
+
+ + + +36 +37 +38+ |
+
+ # File 'lib/wcc/contentful/indexed_representation.rb', line 36 + +def deep_dup + self.class.new(@types.deep_dup) +end+ |
+
+ + + +32 +33 +34+ |
+
+ # File 'lib/wcc/contentful/indexed_representation.rb', line 32 + +def to_json(*args) + @types.to_json(*args) +end+ |
+
%i[ + name + content_type + fields +].freeze
A new instance of ContentType.
+Returns a new instance of ContentType.
+ + +
+ + + +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72+ |
+
+ # File 'lib/wcc/contentful/indexed_representation.rb', line 56 + +def initialize(hash_or_id = nil) + @fields = {} + return unless hash_or_id + + if hash_or_id.is_a?(String) + @name = hash_or_id + return + end + + if raw_fields = (hash_or_id.delete('fields') || hash_or_id.delete(:fields)) + raw_fields.each do |field_name, raw_field| + @fields[field_name] = Field.new(raw_field) + end + end + + hash_or_id.each { |k, v| public_send("#{k}=", v) } +end+ |
+
+ + + +82 +83 +84+ |
+
+ # File 'lib/wcc/contentful/indexed_representation.rb', line 82 + +def ==(other) + ATTRIBUTES.all? { |att| public_send(att) == other.public_send(att) } +end+ |
+
+ + + +74 +75 +76 +77 +78 +79 +80+ |
+
+ # File 'lib/wcc/contentful/indexed_representation.rb', line 74 + +def deep_dup + dup_hash = + ATTRIBUTES.each_with_object({}) do |att, h| + h[att] = public_send(att) + end + self.class.new(dup_hash) +end+ |
+
%i[ + name + type + array + required + link_types +].freeze
%i[ + String + Int + Float + DateTime + Boolean + Json + Coordinates + RichText + Link + Asset +].freeze
A new instance of Field.
+Returns a new instance of Field.
+ + +
+ + + +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138+ |
+
+ # File 'lib/wcc/contentful/indexed_representation.rb', line 117 + +def initialize(hash_or_id = nil) + return unless hash_or_id + + if hash_or_id.is_a?(String) + @name = hash_or_id + return + end + + unless hash_or_id.is_a?(Hash) + ATTRIBUTES.each { |att| public_send("#{att}=", hash_or_id.public_send(att)) } + return + end + + if raw_type = hash_or_id.delete('type') + raw_type = raw_type.to_sym + raise ArgumentError, "Unknown type #{raw_type}, expected one of: #{TYPES}" unless TYPES.include?(raw_type) + + @type = raw_type + end + + hash_or_id.each { |k, v| public_send("#{k}=", v) } +end+ |
+
+ + + +140 +141 +142+ |
+
+ # File 'lib/wcc/contentful/indexed_representation.rb', line 140 + +def ==(other) + ATTRIBUTES.all? { |att| public_send(att) == other.public_send(att) } +end+ |
+
+ + + +111 +112 +113 +114 +115+ |
+
+ # File 'lib/wcc/contentful/indexed_representation.rb', line 111 + +def type=(raw_type) + raise ArgumentError, "Unknown type #{raw_type}, expected one of: #{TYPES}" unless TYPES.include?(raw_type) + + @type = raw_type +end+ |
+
+ + + +16 +17 +18 +19+ |
+
+ # File 'lib/wcc/contentful/instrumentation.rb', line 16 + +def _instrumentation + # look for per-instance instrumentation then try class level + @_instrumentation || self.class._instrumentation +end+ |
+
+ + + +44 +45 +46 +47+ |
+
+ # File 'lib/wcc/contentful/instrumentation.rb', line 44 + +def instrument(name, payload = {}, &block) + WCC::Contentful::Services.instance + .instrumentation.instrument(name, payload, &block) +end+ |
+
+ + + +7 +8 +9 +10 +11 +12+ |
+
+ # File 'lib/wcc/contentful/instrumentation.rb', line 7 + +def _instrumentation_event_prefix + @_instrumentation_event_prefix ||= + # WCC::Contentful => contentful.wcc + '.' + (is_a?(Class) || is_a?(Module) ? self : self.class) # rubocop:disable Style/StringConcatenation + .name.parameterize.split('-').reverse.join('.') +end+ |
+
{ + Asset: 'Asset', + Link: 'Entry' +}.freeze
Returns the value of attribute id.
+Returns the value of attribute link_type.
+Returns the value of attribute raw.
+A new instance of Link.
+Returns a new instance of Link.
+ + +
+ + + +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23+ |
+
+ # File 'lib/wcc/contentful/link.rb', line 11 + +def initialize(model, link_type = nil) + @id = model.try(:id) || model + @link_type = link_type + @link_type ||= model.is_a?(WCC::Contentful::Model::Asset) ? :Asset : :Link + @raw = + { + 'sys' => { + 'type' => 'Link', + 'linkType' => LINK_TYPES[@link_type] || link_type, + 'id' => @id + } + } +end+ |
+
Returns the value of attribute id.
+ + +
+ + + +4 +5 +6+ |
+
+ # File 'lib/wcc/contentful/link.rb', line 4 + +def id + @id +end+ |
+
Returns the value of attribute link_type.
+ + +
+ + + +4 +5 +6+ |
+
+ # File 'lib/wcc/contentful/link.rb', line 4 + +def link_type + @link_type +end+ |
+
Returns the value of attribute raw.
+ + +
+ + + +4 +5 +6+ |
+
+ # File 'lib/wcc/contentful/link.rb', line 4 + +def raw + @raw +end+ |
+
The LinkVisitor is a utility class for walking trees of linked entries. It is used internally by the Store layer to compose the resulting resolved hashes. But you can use it too!
+ + +Returns the value of attribute depth.
+Returns the value of attribute entry.
+Returns the value of attribute fields.
+Walks an entry and its resolved links, without transforming the entry.
+A new instance of LinkVisitor.
+Returns a new instance of LinkVisitor.
+ + +
+ + + +14 +15 +16 +17 +18 +19 +20 +21 +22+ |
+
+ # File 'lib/wcc/contentful/link_visitor.rb', line 14 + +def initialize(entry, *fields, depth: 0) + unless entry.is_a?(Hash) && entry.dig('sys', 'type') == 'Entry' + raise ArgumentError, "Please provide an entry as a hash value (got #{entry})" + end + + @entry = entry + @fields = fields.map(&:to_s) + @depth = depth +end+ |
+
Returns the value of attribute depth.
+ + +
+ + + +7 +8 +9+ |
+
+ # File 'lib/wcc/contentful/link_visitor.rb', line 7 + +def depth + @depth +end+ |
+
Returns the value of attribute entry.
+ + +
+ + + +7 +8 +9+ |
+
+ # File 'lib/wcc/contentful/link_visitor.rb', line 7 + +def entry + @entry +end+ |
+
Returns the value of attribute fields.
+ + +
+ + + +7 +8 +9+ |
+
+ # File 'lib/wcc/contentful/link_visitor.rb', line 7 + +def fields + @fields +end+ |
+
Walks an entry and its resolved links, without transforming the entry.
+ + +
+ + + +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40+ |
+
+ # File 'lib/wcc/contentful/link_visitor.rb', line 30 + +def each(&block) + _each do |val, field, locale, index| + yield(val, field, locale, index) if should_yield_field?(field, val) + + next unless should_walk_link?(field, val) + + self.class.new(val, *fields, depth: depth - 1).each(&block) + end + + nil +end+ |
+
+ + + +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55+ |
+
+ # File 'lib/wcc/contentful/link_visitor.rb', line 42 + +def map!(&block) + _each do |val, field, locale, index| + if should_yield_field?(field, val) + val = yield(val, field, locale, index) + set_field(field, locale, index, val) + end + + next unless should_walk_link?(field, val) + + self.class.new(val, *fields, depth: depth - 1).map!(&block) + end + + entry +end+ |
+
+ + + Modules: Store + + + + +
+ + + + + + + + + +A Store middleware wraps the Store interface to perform any desired transformations on the Contentful entries coming back from the store. A Store middleware must implement the Store interface as well as a `store=` attribute writer, which is used to inject the next store or middleware in the chain.
+ +The Store interface can be seen on the WCC::Contentful::Store::Base class. It consists of the `#find, #find_by, #find_all, #set, #delete,` and `#index` methods.
+ +Including this concern will define those methods to pass through to the next store. Any of those methods can be overridden on the implementing middleware. It will also expose two overridable methods, `#select?` and `#transform`. These methods are applied when reading values out of the store, and can be used to apply a filter or transformation to each entry in the store.
+ + ++ + + + + Classes: CachingMiddleware, DelegatingQuery + + +
+ + + +Store::Interface::INTERFACE_METHODS
+ + +Returns the value of attribute store.
+rubocop:disable Naming/PredicateName.
+The default version of `#transform` just returns the entry.
+Returns the value of attribute store.
+ + +
+ + + +22 +23 +24+ |
+
+ # File 'lib/wcc/contentful/middleware/store.rb', line 22 + +def store + @store +end+ |
+
+ + + +34 +35 +36 +37+ |
+
+ # File 'lib/wcc/contentful/middleware/store.rb', line 34 + +def find(id, **) + found = store.find(id, **) + return transform(found) if found && (!has_select? || select?(found)) +end+ |
+
+ + + +47 +48 +49 +50 +51 +52 +53+ |
+
+ # File 'lib/wcc/contentful/middleware/store.rb', line 47 + +def find_all(options: nil, **args) + DelegatingQuery.new( + store.find_all(**args.merge(options: )), + middleware: self, + options: + ) +end+ |
+
+ + + +39 +40 +41 +42 +43 +44 +45+ |
+
+ # File 'lib/wcc/contentful/middleware/store.rb', line 39 + +def find_by(options: nil, **args) + result = store.find_by(**args.merge(options: )) + return unless result && (!has_select? || select?(result)) + + result = resolve_includes(result, [:include]) if && [:include] + transform(result) +end+ |
+
rubocop:disable Naming/PredicateName
+ + +
+ + + +80 +81 +82+ |
+
+ # File 'lib/wcc/contentful/middleware/store.rb', line 80 + +def has_select? # rubocop:disable Naming/PredicateName + respond_to?(:select?) +end+ |
+
+ + + +55 +56 +57 +58 +59 +60 +61 +62+ |
+
+ # File 'lib/wcc/contentful/middleware/store.rb', line 55 + +def resolve_includes(entry, depth) + return entry unless entry && depth && depth > 0 + + # We only care about entries (see #resolved_link?) + WCC::Contentful::LinkVisitor.new(entry, :Entry, depth: depth).map! do |val| + resolve_link(val) + end +end+ |
+
+ + + +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74+ |
+
+ # File 'lib/wcc/contentful/middleware/store.rb', line 64 + +def resolve_link(val) + return val unless resolved_link?(val) + + if !has_select? || select?(val) + transform(val) + else + # Pretend it's an unresolved link - + # matches the behavior of a store when the link cannot be retrieved + WCC::Contentful::Link.new(val.dig('sys', 'id'), val.dig('sys', 'type')).to_h + end +end+ |
+
+ + + +76 +77 +78+ |
+
+ # File 'lib/wcc/contentful/middleware/store.rb', line 76 + +def resolved_link?(value) + value.is_a?(Hash) && value.dig('sys', 'type') == 'Entry' +end+ |
+
The default version of `#transform` just returns the entry. Override this with your own implementation.
+ + +
+ + + +86 +87 +88+ |
+
+ # File 'lib/wcc/contentful/middleware/store.rb', line 86 + +def transform(entry) + entry +end+ |
+
Store::Interface::INTERFACE_METHODS
+ + +Returns the value of attribute expires_in.
+TODO: github.com/watermarkchurch/wcc-contentful/issues/18 figure out how to cache the results of a find_by query, ex: `find_by('slug' => '/about')`.
+#index is called whenever the sync API comes back with more data.
+A new instance of CachingMiddleware.
+#_instrumentation_event_prefix, instrument
+ + + + + + + + + + +#find_all, #has_select?, #resolve_includes, #resolve_link, #resolved_link?, #transform
+ + + + + + + + + + +Returns a new instance of CachingMiddleware.
+ + +
+ + + +11 +12 +13 +14+ |
+
+ # File 'lib/wcc/contentful/middleware/store/caching_middleware.rb', line 11 + +def initialize(cache = nil) + @cache = cache || ActiveSupport::Cache::MemoryStore.new + @expires_in = nil +end+ |
+
Returns the value of attribute expires_in.
+ + +
+ + + +9 +10 +11+ |
+
+ # File 'lib/wcc/contentful/middleware/store/caching_middleware.rb', line 9 + +def expires_in + @expires_in +end+ |
+
+ + + +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33+ |
+
+ # File 'lib/wcc/contentful/middleware/store/caching_middleware.rb', line 16 + +def find(key, **) + event = 'fresh' + found = + @cache.fetch(key, expires_in: expires_in) do + event = 'miss' + # if it's not a contentful ID don't hit the API. + # Store a nil object if we can't find the object on the CDN. + (store.find(key, **) || nil_obj(key)) if key =~ /^\w+$/ + end + _instrument(event, key: key, options: ) + + case found.try(:dig, 'sys', 'type') + when 'Nil', 'DeletedEntry', 'DeletedAsset' + nil + else + found + end +end+ |
+
TODO: github.com/watermarkchurch/wcc-contentful/issues/18
+ +figure out how to cache the results of a find_by query, ex:
+`find_by('slug' => '/about')`
+
+
+
+
+ + + +38 +39 +40 +41 +42 +43 +44+ |
+
+ # File 'lib/wcc/contentful/middleware/store/caching_middleware.rb', line 38 + +def find_by(content_type:, filter: nil, options: nil) + if filter&.keys == ['sys.id'] && found = @cache.read(filter['sys.id']) + return found + end + + store.find_by(content_type: content_type, filter: filter, options: ) +end+ |
+
#index is called whenever the sync API comes back with more data.
+ + +
+ + + +49 +50 +51 +52 +53 +54 +55 +56+ |
+
+ # File 'lib/wcc/contentful/middleware/store/caching_middleware.rb', line 49 + +def index(json) + delegated_result = store.index(json) if store.index? + caching_result = _index(json) + # _index returns nil if we don't already have it cached - so use the store result. + # store result is nil if it doesn't index, so use the caching result if we have it. + # They ought to be the same thing if it's cached and the store also indexes. + caching_result || delegated_result +end+ |
+
+ + + +58 +59 +60+ |
+
+ # File 'lib/wcc/contentful/middleware/store/caching_middleware.rb', line 58 + +def index? + true +end+ |
+
Store::Query::Interface::OPERATORS
+ + +Returns the value of attribute middleware.
+Returns the value of attribute options.
+Returns the value of attribute wrapped_query.
+A new instance of DelegatingQuery.
+Returns a new instance of DelegatingQuery.
+ + +
+ + + +150 +151 +152 +153 +154 +155+ |
+
+ # File 'lib/wcc/contentful/middleware/store.rb', line 150 + +def initialize(wrapped_query, middleware:, options: nil, **extra) + @wrapped_query = wrapped_query + @middleware = middleware + @options = + @extra = extra +end+ |
+
Returns the value of attribute middleware.
+ + +
+ + + +109 +110 +111+ |
+
+ # File 'lib/wcc/contentful/middleware/store.rb', line 109 + +def middleware + @middleware +end+ |
+
Returns the value of attribute options.
+ + +
+ + + +109 +110 +111+ |
+
+ # File 'lib/wcc/contentful/middleware/store.rb', line 109 + +def + @options +end+ |
+
Returns the value of attribute wrapped_query.
+ + +
+ + + +109 +110 +111+ |
+
+ # File 'lib/wcc/contentful/middleware/store.rb', line 109 + +def wrapped_query + @wrapped_query +end+ |
+
+ + + +120 +121 +122 +123 +124 +125 +126 +127+ |
+
+ # File 'lib/wcc/contentful/middleware/store.rb', line 120 + +def apply(filter, context = nil) + self.class.new( + wrapped_query.apply(filter, context), + middleware: middleware, + options: , + **@extra + ) +end+ |
+
+ + + +129 +130 +131 +132 +133 +134 +135 +136+ |
+
+ # File 'lib/wcc/contentful/middleware/store.rb', line 129 + +def apply_operator(operator, field, expected, context = nil) + self.class.new( + wrapped_query.apply_operator(operator, field, expected, context), + middleware: middleware, + options: , + **@extra + ) +end+ |
+
+ + + +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107+ |
+
+ # File 'lib/wcc/contentful/middleware/store.rb', line 97 + +def count + if middleware.has_select? + raise NameError, "Count cannot be determined because the middleware '#{middleware}' " \ + "implements the #select? method. Please use '.to_a.count' to count entries that " \ + 'pass the #select? method.' + end + + # The wrapped query may get count from the "Total" field in the response, + # or apply a "COUNT(*)" to the query. + wrapped_query.count +end+ |
+
+ + + +111 +112 +113 +114 +115 +116 +117 +118+ |
+
+ # File 'lib/wcc/contentful/middleware/store.rb', line 111 + +def to_enum + result = wrapped_query.to_enum + result = result.select { |x| middleware.select?(x) } if middleware.has_select? + + result = result.map { |x| middleware.resolve_includes(x, [:include]) } if && [:include] + + result.map { |x| middleware.transform(x) } +end+ |
+
This is the top layer of the WCC::Contentful gem. It exposes an API by which you can query for data from Contentful. The API is only accessible after calling WCC::Contentful.init!
+ +The WCC::Contentful::Model class is the base class for all auto-generated model classes. A model class represents a content type inside Contentful. For example, the “page” content type is represented by a class named WCC::Contentful::Model::Page
+ +This WCC::Contentful::Model::Page class exposes the following API methods:
+Page.find(id) finds a single Page by it's ID
+Page.find_by(field: <value>) finds a single Page with the matching value for the specified field
+Page.find_all(field: <value>) finds all instances of Page with the matching value for the specified field. It returns a lazy iterator of Page objects.
+The returned objects are instances of WCC::Contentful::Model::Page, or whatever constant exists in the registry for the page content type. You can register custom types to be instantiated for each content type. If a Model is subclassed, the subclass is automatically registered. This allows you to put models in your app's `app/models` directory:
+ +class Page < WCC::Contentful::Model::Page; end
+
+
+and then use the API via those models:
+ +# this returns a ::Page, not a WCC::Contentful::Model::Page
+Page.find_by(slug: 'foo')
+
+
+Furthermore, anytime links are automatically resolved, the registered classes will be used:
+ +Menu.find_by(name: 'home')..first.linked_page # is a ::Page
+
+
+
+
+ + + +45 +46 +47 +48 +49+ |
+
+ # File 'lib/wcc/contentful/model.rb', line 45 + +def const_missing(name) + type = WCC::Contentful::Helpers.content_type_from_constant(name) + raise WCC::Contentful::ContentTypeNotFoundError, + "Content type '#{type}' does not exist in the space" +end+ |
+
Returns the value of attribute namespace.
+A new instance of ModelBuilder.
+#constant_from_content_type, #content_type_from_constant, #content_type_from_raw, #shared_prefix
+Returns a new instance of ModelBuilder.
+ + +
+ + + +13 +14 +15 +16+ |
+
+ # File 'lib/wcc/contentful/model_builder.rb', line 13 + +def initialize(types, namespace: WCC::Contentful::Model) + @types = types + @namespace = namespace +end+ |
+
Returns the value of attribute namespace.
+ + +
+ + + +11 +12 +13+ |
+
+ # File 'lib/wcc/contentful/model_builder.rb', line 11 + +def namespace + @namespace +end+ |
+
+ + + +18 +19 +20 +21 +22+ |
+
+ # File 'lib/wcc/contentful/model_builder.rb', line 18 + +def build_models + @types.each_with_object([]) do |(_k, v), a| + a << build_model(v) + end +end+ |
+
This module is included by all models and defines instance methods that are not dynamically generated.
+ + +The set of options keys that are specific to the Model layer and shouldn't be passed down to the Store layer.
+ + +%i[ + preview + backlinks +].freeze
Resolves all links in an entry to the specified depth.
+Determines whether the object has been resolved up to the prescribed depth.
+Turns the current model into a hash representation as though it had been retrieved from the Contentful API.
+Resolves all links in an entry to the specified depth.
+ +Each link in the entry is recursively retrieved from the store until the given depth is satisfied. Depth resolution is unlimited, circular references will be resolved to the same object.
+ + +
+ + + +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70+ |
+
+ # File 'lib/wcc/contentful/model_methods.rb', line 32 + +def resolve(depth: 1, fields: nil, context: sys.context.to_h, **) + raise ArgumentError, "Depth must be > 0 (was #{depth})" unless depth && depth > 0 + return self if resolved?(depth: depth, fields: fields) + + fields = fields.map { |f| f.to_s.camelize(:lower) } if fields.present? + fields ||= self.class::FIELDS + + typedef = self.class.content_type_definition + links = fields.select { |f| %i[Asset Link].include?(typedef.fields[f].type) } + store = context[:preview] ? self.class.services.preview_store : self.class.services.store + + raw_link_ids = + links.map { |field_name| raw.dig('fields', field_name, sys.locale) } + .flat_map do |raw_value| + _try_map(raw_value) { |v| v.dig('sys', 'id') if v.dig('sys', 'type') == 'Link' } + end + raw_link_ids = raw_link_ids.compact + backlinked_ids = (context[:backlinks]&.map { |m| m.id } || []) + + has_unresolved_raw_links = (raw_link_ids - backlinked_ids).any? + if has_unresolved_raw_links + raw = + _instrument 'resolve', id: id, depth: depth, backlinks: backlinked_ids do + # use include param to do resolution + store.find_by(content_type: self.class.content_type, + filter: { 'sys.id' => id }, + options: context.except(*MODEL_LAYER_CONTEXT_KEYS).merge!({ + include: [depth, 10].min + })) + end + raise WCC::Contentful::ResolveError, "Cannot find #{self.class.content_type} with ID #{id}" unless raw + + @raw = raw.freeze + links.each { |f| instance_variable_set("@#{f}", raw.dig('fields', f, sys.locale)) } + end + + links.each { |f| _resolve_field(f, depth, context, ) } + self +end+ |
+
Determines whether the object has been resolved up to the prescribed depth.
+ + +
+ + + +73 +74 +75 +76 +77 +78 +79 +80 +81 +82+ |
+
+ # File 'lib/wcc/contentful/model_methods.rb', line 73 + +def resolved?(depth: 1, fields: nil) + raise ArgumentError, "Depth must be > 0 (was #{depth})" unless depth && depth > 0 + + fields = fields.map { |f| f.to_s.camelize(:lower) } if fields.present? + fields ||= self.class::FIELDS + + typedef = self.class.content_type_definition + links = fields.select { |f| %i[Asset Link].include?(typedef.fields[f].type) } + links.all? { |f| _resolved_field?(f, depth) } +end+ |
+
Turns the current model into a hash representation as though it had been retrieved from the Contentful API.
+ +This differs from `#raw` in that it recursively includes the `#to_h` of resolved links. It also sets the fields to the value for the entry's `#sys.locale`, as though the entry had been retrieved from the API with `locale=WCC::Contentful::ModelMethods#sys#sys.locale` rather than `locale=*`.
+ + +
+ + + +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128+ |
+
+ # File 'lib/wcc/contentful/model_methods.rb', line 91 + +def to_h(stack = nil) + raise WCC::Contentful::CircularReferenceError.new(stack, id) if stack&.include?(id) + + stack = [*stack, id] + typedef = self.class.content_type_definition + fields = + typedef.fields.each_with_object({}) do |(name, field_def), h| + if field_def.type == :Link || field_def.type == :Asset + if _resolved_field?(name, 0) + val = public_send(name) + val = + _try_map(val) { |v| v.to_h(stack) } + else + ids = field_def.array ? public_send("#{name}_ids") : public_send("#{name}_id") + val = + _try_map(ids) do |id| + { + 'sys' => { + 'type' => 'Link', + 'linkType' => field_def.type == :Asset ? 'Asset' : 'Entry', + 'id' => id + } + } + end + end + else + val = public_send(name) + val = _try_map(val) { |v| v.respond_to?(:to_h) ? v.to_h.stringify_keys! : v } + end + + h[name] = val + end + + { + 'sys' => { 'locale' => @sys.locale }.merge!(@raw['sys']), + 'fields' => fields + } +end+ |
+
This module is extended by all models and defines singleton methods that are not dynamically generated.
+ + ++ + + + + Classes: ModelQuery + + +
+ + + + + + + + +Finds an instance of this content type.
+Finds all instances of this content type, optionally limiting to those matching a given filter query.
+Finds the first instance of this content type matching the given query.
+rubocop:disable Lint/MissingSuper.
+Finds an instance of this content type.
+ + +
+ + + +13 +14 +15 +16 +17 +18 +19 +20 +21 +22+ |
+
+ # File 'lib/wcc/contentful/model_singleton_methods.rb', line 13 + +def find(id, options: nil) + ||= {} + store = [:preview] ? services.preview_store : services.store + raw = + _instrumentation.instrument 'find.model.contentful.wcc', + content_type: content_type, id: id, options: do + store.find(id, **{ hint: type }.merge!(.except(:preview))) + end + new(raw, ) if raw.present? +end+ |
+
Finds all instances of this content type, optionally limiting to those matching a given filter query.
+ + +
+ + + +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45+ |
+
+ # File 'lib/wcc/contentful/model_singleton_methods.rb', line 31 + +def find_all(filter = nil) + filter = filter&.dup + = filter&.delete(:options) || {} + + filter.transform_keys! { |k| k.to_s.camelize(:lower) } if filter.present? + + store = [:preview] ? services.preview_store : services.store + query = + _instrumentation.instrument 'find_all.model.contentful.wcc', + content_type: content_type, filter: filter, options: do + store.find_all(content_type: content_type, options: .except(:preview)) + end + query = query.apply(filter) if filter.present? + ModelQuery.new(query, , self) +end+ |
+
Finds the first instance of this content type matching the given query.
+ + +
+ + + +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67+ |
+
+ # File 'lib/wcc/contentful/model_singleton_methods.rb', line 53 + +def find_by(filter = nil) + filter = filter&.dup + = filter&.delete(:options) || {} + + filter.transform_keys! { |k| k.to_s.camelize(:lower) } if filter.present? + + store = [:preview] ? services.preview_store : services.store + result = + _instrumentation.instrument 'find_by.model.contentful.wcc', + content_type: content_type, filter: filter, options: do + store.find_by(content_type: content_type, filter: filter, options: .except(:preview)) + end + + new(result, ) if result +end+ |
+
rubocop:disable Lint/MissingSuper
+ + +
+ + + +69 +70 +71 +72 +73 +74 +75+ |
+
+ # File 'lib/wcc/contentful/model_singleton_methods.rb', line 69 + +def inherited(subclass) # rubocop:disable Lint/MissingSuper + # If another different class is already registered for this content type, + # don't auto-register this one. + return if model_namespace.registered?(content_type) + + model_namespace.register_for_content_type(content_type, klass: subclass) +end+ |
+
A new instance of ModelQuery.
+Returns a new instance of ModelQuery.
+ + +
+ + + +89 +90 +91 +92 +93+ |
+
+ # File 'lib/wcc/contentful/model_singleton_methods.rb', line 89 + +def initialize(wrapped_query, , klass) + @wrapped_query = wrapped_query + @options = + @klass = klass +end+ |
+
+ + + +87 +88 +89+ |
+
+ # File 'lib/wcc/contentful/model_singleton_methods.rb', line 87 + +def klass + @klass +end+ |
+
+ + + +87 +88 +89+ |
+
+ # File 'lib/wcc/contentful/model_singleton_methods.rb', line 87 + +def + @options +end+ |
+
+ + + +87 +88 +89+ |
+
+ # File 'lib/wcc/contentful/model_singleton_methods.rb', line 87 + +def wrapped_query + @wrapped_query +end+ |
+
+ + + +95 +96 +97 +98+ |
+
+ # File 'lib/wcc/contentful/model_singleton_methods.rb', line 95 + +def to_enum + wrapped_query.to_enum + .map { |r| klass.new(r, ) } +end+ |
+
Builds out a fake Contentful entry for the given content type, and then stubs the Model API to return that content type for `.find` and `.find_by` query methods.
+#contentful_double, #contentful_image_double
+ + +Builds out a fake Contentful entry for the given content type, and then stubs the Model API to return that content type for `.find` and `.find_by` query methods.
+ + +
+ + + +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39+ |
+
+ # File 'lib/wcc/contentful/rspec.rb', line 15 + +def contentful_stub(const, **attrs) + const = WCC::Contentful::Model.resolve_constant(const.to_s) unless const.respond_to?(:content_type_definition) + instance = contentful_create(const, **attrs) + + # mimic what's going on inside model_singleton_methods.rb + # find, find_by, etc always return a new instance from the same raw + allow(WCC::Contentful::Model).to receive(:find) + .with(instance.id, any_args) do |_id, keyword_params| + = keyword_params && keyword_params[:options] + contentful_create(const, , raw: instance.raw, **attrs) + end + allow(const).to receive(:find) { |id, | WCC::Contentful::Model.find(id, **( || {})) } + + attrs.each do |k, v| + allow(const).to receive(:find_by) + .with(hash_including(k => v)) do |filter| + filter = filter&.dup + = filter&.delete(:options) || {} + + contentful_create(const, , raw: instance.raw, **attrs) + end + end + + instance +end+ |
+
Raised by Model#resolve when attempting to resolve an entry's links and that entry cannot be found in the space.
+ + +This module contains a number of structs representing nodes in a Contentful rich text field. When the Model layer parses a Rich Text field from Contentful, it is turned into a WCC::Contentful::RichText::Document node. The content method of this node is an Array containing paragraph, blockquote, entry, and other nodes.
+ +The various structs in the RichText object model are designed to mimic the Hash interface, so that the indexing operator `#[]` and the `#dig` method can be used to traverse the data. The data can also be accessed by the attribute reader methods defined on the structs. Both of these are considered part of the public API of the model and will not change.
+ +In a future release we plan to implement automatic link resolution. When that happens, the `.data` attribute of embedded entries and assets will return a new class that is able to resolve the `.target` automatically into a full entry or asset. This future class will still respect the hash accessor methods `#[]`, `#dig`, `#keys`, and `#each`, so it is safe to use those.
+ + ++ + + Modules: Node + + + + Classes: Blockquote, Document, EmbeddedAssetBlock, EmbeddedEntryBlock, EmbeddedEntryInline, Paragraph, Text, Unknown + + +
+ + + + + + + + +Recursively converts a raw JSON-parsed hash into the RichText object model.
+Recursively converts a raw JSON-parsed hash into the RichText object model.
+ + +
+ + + +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54+ |
+
+ # File 'lib/wcc/contentful/rich_text.rb', line 26 + +def self.tokenize(raw, context = nil) + return unless raw + return raw.map { |c| tokenize(c, context) } if raw.is_a?(Array) + + klass = + case raw['nodeType'] + when 'document' + Document + when 'paragraph' + Paragraph + when 'blockquote' + Blockquote + when 'text' + Text + when 'embedded-entry-inline' + EmbeddedEntryInline + when 'embedded-entry-block' + EmbeddedEntryBlock + when 'embedded-asset-block' + EmbeddedAssetBlock + when /heading-(\d+)/ + size = Regexp.last_match(1) + const_get("Heading#{size}") + else + Unknown + end + + klass.tokenize(raw, context) +end+ |
+
Returns the value of attribute content.
+Returns the value of attribute data.
+Returns the value of attribute nodeType.
+Returns the value of attribute content
+ + +
+ + + +66 +67 +68+ |
+
+ # File 'lib/wcc/contentful/rich_text.rb', line 66 + +def content + @content +end+ |
+
Returns the value of attribute data
+ + +
+ + + +66 +67 +68+ |
+
+ # File 'lib/wcc/contentful/rich_text.rb', line 66 + +def data + @data +end+ |
+
Returns the value of attribute nodeType
+ + +
+ + + +66 +67 +68+ |
+
+ # File 'lib/wcc/contentful/rich_text.rb', line 66 + +def nodeType + @nodeType +end+ |
+
Returns the value of attribute content.
+Returns the value of attribute data.
+Returns the value of attribute nodeType.
+Returns the value of attribute content
+ + +
+ + + +56 +57 +58+ |
+
+ # File 'lib/wcc/contentful/rich_text.rb', line 56 + +def content + @content +end+ |
+
Returns the value of attribute data
+ + +
+ + + +56 +57 +58+ |
+
+ # File 'lib/wcc/contentful/rich_text.rb', line 56 + +def data + @data +end+ |
+
Returns the value of attribute nodeType
+ + +
+ + + +56 +57 +58+ |
+
+ # File 'lib/wcc/contentful/rich_text.rb', line 56 + +def nodeType + @nodeType +end+ |
+
Returns the value of attribute content.
+Returns the value of attribute data.
+Returns the value of attribute nodeType.
+Returns the value of attribute content
+ + +
+ + + +86 +87 +88+ |
+
+ # File 'lib/wcc/contentful/rich_text.rb', line 86 + +def content + @content +end+ |
+
Returns the value of attribute data
+ + +
+ + + +86 +87 +88+ |
+
+ # File 'lib/wcc/contentful/rich_text.rb', line 86 + +def data + @data +end+ |
+
Returns the value of attribute nodeType
+ + +
+ + + +86 +87 +88+ |
+
+ # File 'lib/wcc/contentful/rich_text.rb', line 86 + +def nodeType + @nodeType +end+ |
+
Returns the value of attribute content.
+Returns the value of attribute data.
+Returns the value of attribute nodeType.
+Returns the value of attribute content
+ + +
+ + + +81 +82 +83+ |
+
+ # File 'lib/wcc/contentful/rich_text.rb', line 81 + +def content + @content +end+ |
+
Returns the value of attribute data
+ + +
+ + + +81 +82 +83+ |
+
+ # File 'lib/wcc/contentful/rich_text.rb', line 81 + +def data + @data +end+ |
+
Returns the value of attribute nodeType
+ + +
+ + + +81 +82 +83+ |
+
+ # File 'lib/wcc/contentful/rich_text.rb', line 81 + +def nodeType + @nodeType +end+ |
+
Returns the value of attribute content.
+Returns the value of attribute data.
+Returns the value of attribute nodeType.
+Returns the value of attribute content
+ + +
+ + + +76 +77 +78+ |
+
+ # File 'lib/wcc/contentful/rich_text.rb', line 76 + +def content + @content +end+ |
+
Returns the value of attribute data
+ + +
+ + + +76 +77 +78+ |
+
+ # File 'lib/wcc/contentful/rich_text.rb', line 76 + +def data + @data +end+ |
+
Returns the value of attribute nodeType
+ + +
+ + + +76 +77 +78+ |
+
+ # File 'lib/wcc/contentful/rich_text.rb', line 76 + +def nodeType + @nodeType +end+ |
+
+ + + +7 +8 +9+ |
+
+ # File 'lib/wcc/contentful/rich_text/node.rb', line 7 + +def keys + members.map(&:to_s) +end+ |
+
Returns the value of attribute content.
+Returns the value of attribute data.
+Returns the value of attribute nodeType.
+Returns the value of attribute content
+ + +
+ + + +61 +62 +63+ |
+
+ # File 'lib/wcc/contentful/rich_text.rb', line 61 + +def content + @content +end+ |
+
Returns the value of attribute data
+ + +
+ + + +61 +62 +63+ |
+
+ # File 'lib/wcc/contentful/rich_text.rb', line 61 + +def data + @data +end+ |
+
Returns the value of attribute nodeType
+ + +
+ + + +61 +62 +63+ |
+
+ # File 'lib/wcc/contentful/rich_text.rb', line 61 + +def nodeType + @nodeType +end+ |
+
Returns the value of attribute data.
+Returns the value of attribute marks.
+Returns the value of attribute nodeType.
+Returns the value of attribute value.
+Returns the value of attribute data
+ + +
+ + + +71 +72 +73+ |
+
+ # File 'lib/wcc/contentful/rich_text.rb', line 71 + +def data + @data +end+ |
+
Returns the value of attribute marks
+ + +
+ + + +71 +72 +73+ |
+
+ # File 'lib/wcc/contentful/rich_text.rb', line 71 + +def marks + @marks +end+ |
+
Returns the value of attribute nodeType
+ + +
+ + + +71 +72 +73+ |
+
+ # File 'lib/wcc/contentful/rich_text.rb', line 71 + +def nodeType + @nodeType +end+ |
+
Returns the value of attribute value
+ + +
+ + + +71 +72 +73+ |
+
+ # File 'lib/wcc/contentful/rich_text.rb', line 71 + +def value + @value +end+ |
+
Returns the value of attribute content.
+Returns the value of attribute data.
+Returns the value of attribute nodeType.
+Returns the value of attribute content
+ + +
+ + + +101 +102 +103+ |
+
+ # File 'lib/wcc/contentful/rich_text.rb', line 101 + +def content + @content +end+ |
+
Returns the value of attribute data
+ + +
+ + + +101 +102 +103+ |
+
+ # File 'lib/wcc/contentful/rich_text.rb', line 101 + +def data + @data +end+ |
+
Returns the value of attribute nodeType
+ + +
+ + + +101 +102 +103+ |
+
+ # File 'lib/wcc/contentful/rich_text.rb', line 101 + +def nodeType + @nodeType +end+ |
+
Include this module to define accessors for every method defined on the Services singleton.
+ + +Returns the value of attribute configuration.
+Gets a CDN Client which provides methods for getting and paging raw JSON data from the Contentful CDN.
+A new instance of Services.
+This method enables simple dependency injection - If the target has a setter matching the name of one of the services, set that setter with the value of the service.
+Gets the configured instrumentation adapter, defaulting to ActiveSupport::Notifications.
+Gets the configured logger, defaulting to Rails.logger in a rails context, or logging to STDERR in a non-rails context.
+Gets a Management Client which provides methods for updating data via the Contentful Management API.
+Gets a CDN Client which provides methods for getting and paging raw JSON data from the Contentful Preview API.
+An instance of WCC::Contentful::Store::CDNAdapter which connects to the Contentful Preview API to return preview content.
+Gets the data-store which executes the queries run against the dynamic models in the WCC::Contentful::Model namespace.
+Gets the configured WCC::Contentful::SyncEngine which is responsible for updating the currently configured store.
+Returns a new instance of Services.
+ + +
+ + + +14 +15 +16 +17 +18+ |
+
+ # File 'lib/wcc/contentful/services.rb', line 14 + +def initialize(configuration) + raise ArgumentError, 'Not yet configured!' unless configuration + + @configuration = configuration +end+ |
+
Returns the value of attribute configuration.
+ + +
+ + + +12 +13 +14+ |
+
+ # File 'lib/wcc/contentful/services.rb', line 12 + +def configuration + @configuration +end+ |
+
+ + + +6 +7 +8 +9+ |
+
+ # File 'lib/wcc/contentful/services.rb', line 6 + +def instance + @singleton__instance__ ||= # rubocop:disable Naming/MemoizedInstanceVariableName + (new(WCC::Contentful.configuration) if WCC::Contentful.configuration) +end+ |
+
Gets a CDN Client which provides methods for getting and paging raw JSON data from the Contentful CDN.
+ + +
+ + + +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68+ |
+
+ # File 'lib/wcc/contentful/services.rb', line 57 + +def client + @client ||= + WCC::Contentful::SimpleClient::Cdn.new( + **configuration., + access_token: configuration.access_token, + space: configuration.space, + default_locale: configuration.default_locale, + connection: configuration.connection, + environment: configuration.environment, + instrumentation: instrumentation + ) +end+ |
+
This method enables simple dependency injection - If the target has a setter matching the name of one of the services, set that setter with the value of the service.
+ + +
+ + + +148 +149 +150 +151 +152 +153 +154 +155+ |
+
+ # File 'lib/wcc/contentful/services.rb', line 148 + +def inject_into(target, except: []) + (WCC::Contentful::SERVICES - except).each do |s| + next unless target.respond_to?("#{s}=") + + target.public_send("#{s}=", + public_send(s)) + end +end+ |
+
Gets the configured instrumentation adapter, defaulting to ActiveSupport::Notifications
+ + +
+ + + +127 +128 +129 +130 +131+ |
+
+ # File 'lib/wcc/contentful/services.rb', line 127 + +def instrumentation + @instrumentation ||= + configuration.instrumentation_adapter || + ActiveSupport::Notifications +end+ |
+
Gets the configured logger, defaulting to Rails.logger in a rails context, or logging to STDERR in a non-rails context.
+ + +
+ + + +137 +138 +139 +140 +141 +142+ |
+
+ # File 'lib/wcc/contentful/services.rb', line 137 + +def logger + @logger ||= + configuration.logger || + (Rails.logger if defined?(Rails)) || + Logger.new($stderr) +end+ |
+
Gets a Management Client which provides methods for updating data via the Contentful Management API
+ + +
+ + + +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106+ |
+
+ # File 'lib/wcc/contentful/services.rb', line 93 + +def management_client + @management_client ||= + if configuration.management_token.present? + WCC::Contentful::SimpleClient::Management.new( + **configuration., + management_token: configuration.management_token, + space: configuration.space, + default_locale: configuration.default_locale, + connection: configuration.connection, + environment: configuration.environment, + instrumentation: instrumentation + ) + end +end+ |
+
Gets a CDN Client which provides methods for getting and paging raw JSON data from the Contentful Preview API.
+ + +
+ + + +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87+ |
+
+ # File 'lib/wcc/contentful/services.rb', line 74 + +def preview_client + @preview_client ||= + if configuration.preview_token.present? + WCC::Contentful::SimpleClient::Preview.new( + **configuration., + preview_token: configuration.preview_token, + space: configuration.space, + default_locale: configuration.default_locale, + connection: configuration.connection, + environment: configuration.environment, + instrumentation: instrumentation + ) + end +end+ |
+
An instance of WCC::Contentful::Store::CDNAdapter which connects to the Contentful Preview API to return preview content.
+ + +
+ + + +44 +45 +46 +47 +48 +49 +50 +51+ |
+
+ # File 'lib/wcc/contentful/services.rb', line 44 + +def preview_store + @preview_store ||= + WCC::Contentful::Store::Factory.new( + configuration, + :direct, + :preview + ).build(self) +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 store method:
+an instance of WCC::Contentful::Store::CDNAdapter with a CDN Client to access the CDN.
+an instance of Middleware::Store::CachingMiddleware with the configured ActiveSupport::Cache implementation around a WCC::Contentful::Store::CDNAdapter for when data cannot be found in the cache.
+an instance of the configured Store type, defined by Configuration#sync_store
+
+ + + +36 +37 +38+ |
+
+ # File 'lib/wcc/contentful/services.rb', line 36 + +def store + @store ||= configuration.store.build(self) +end+ |
+
Gets the configured WCC::Contentful::SyncEngine which is responsible for updating the currently configured store. The application must periodically call #next on this instance. Alternately, the application can mount the WCC::Contentful::Engine, which will call #next anytime a webhook is received.
+ +This returns `nil` if the currently configured store does not respond to sync events.
+ + +
+ + + +115 +116 +117 +118 +119 +120 +121 +122 +123 +124+ |
+
+ # File 'lib/wcc/contentful/services.rb', line 115 + +def sync_engine + @sync_engine ||= + if store.index? + SyncEngine.new( + store: store, + client: client, + key: 'sync:token' + ) + end +end+ |
+
The SimpleClient accesses the Contentful CDN to get JSON responses, returning the raw JSON data as a parsed hash. This is the bottom layer of the WCC::Contentful gem.
+ +Note: Do not create this directly, instead create one of WCC::Contentful::SimpleClient::Cdn, WCC::Contentful::SimpleClient::Preview, WCC::Contentful::SimpleClient::Management
+ +It can be configured to access any API url and exposes only a single method, `get`. This method returns a WCC::Contentful::SimpleClient::Response that handles paging automatically.
+ +The SimpleClient by default uses 'faraday' to perform the gets, but any HTTP client adapter be injected by passing the `connection:` option.
+ + ++ + + + + Classes: ApiError, Cdn, Management, NotFoundError, PaginatingEnumerable, Preview, RateLimitError, Response, SyncResponse, TyphoeusAdapter, UnauthorizedError + + +
+ + +{ + faraday: ['faraday', '>= 0.9'], + typhoeus: ['typhoeus', '~> 1.0'] +}.freeze
performs an HTTP GET request to the specified path within the configured space and environment.
+Creates a new SimpleClient with the given configuration.
+Creates a new SimpleClient with the given configuration.
+ + +
+ + + +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62+ |
+
+ # File 'lib/wcc/contentful/simple_client.rb', line 44 + +def initialize(api_url:, space:, access_token:, **) + @api_url = URI.join(api_url, '/spaces/', "#{space}/") + @space = space + @access_token = access_token + + @adapter = SimpleClient.load_adapter([:connection]) + + @options = + @_instrumentation = @options[:instrumentation] + @query_defaults = {} + @query_defaults[:locale] = @options[:default_locale] if @options[:default_locale] + # default 1.5 so that we retry one time then fail if still rate limited + # https://www.contentful.com/developers/docs/references/content-preview-api/#/introduction/api-rate-limits + @rate_limit_wait_timeout = @options[:rate_limit_wait_timeout] || 1.5 + + return unless [:environment].present? + + @api_url = URI.join(@api_url, 'environments/', "#{[:environment]}/") +end+ |
+
+ + + +29 +30 +31+ |
+
+ # File 'lib/wcc/contentful/simple_client.rb', line 29 + +def api_url + @api_url +end+ |
+
+ + + +29 +30 +31+ |
+
+ # File 'lib/wcc/contentful/simple_client.rb', line 29 + +def space + @space +end+ |
+
+ + + +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111+ |
+
+ # File 'lib/wcc/contentful/simple_client.rb', line 84 + +def self.load_adapter(adapter) + case adapter + when nil + ADAPTERS.each do |a, spec| + gem(*spec) + return load_adapter(a) + rescue Gem::LoadError + next + end + raise ArgumentError, 'Unable to load adapter! Please install one of ' \ + "#{ADAPTERS.values.map(&:join).join(',')}" + when :faraday + require 'faraday' + ::Faraday.new do |faraday| + faraday.response :logger, (Rails.logger if defined?(Rails)), { headers: false, bodies: false } + faraday.adapter :net_http + end + when :typhoeus + require_relative 'simple_client/typhoeus_adapter' + TyphoeusAdapter.new + else + unless adapter.respond_to?(:get) + raise ArgumentError, "Adapter #{adapter} is not invokeable! Please " \ + "pass use one of #{ADAPTERS.keys} or create a Faraday-compatible adapter" + end + adapter + end +end+ |
+
performs an HTTP GET request to the specified path within the configured space and environment. Query parameters are merged with the defaults and appended to the request.
+ + +
+ + + +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77+ |
+
+ # File 'lib/wcc/contentful/simple_client.rb', line 67 + +def get(path, query = {}) + url = URI.join(@api_url, path) + + resp = + _instrument 'get_http', url: url, query: query do + get_http(url, query) + end + Response.new(self, + { url: url, query: query }, + resp) +end+ |
+
A new instance of ApiError.
+Returns a new instance of ApiError.
+ + +
+ + + +210 +211 +212 +213+ |
+
+ # File 'lib/wcc/contentful/simple_client/response.rb', line 210 + +def initialize(response) + @response = response + super(response.) +end+ |
+
+ + + +195 +196 +197+ |
+
+ # File 'lib/wcc/contentful/simple_client/response.rb', line 195 + +def response + @response +end+ |
+
+ + + +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208+ |
+
+ # File 'lib/wcc/contentful/simple_client/response.rb', line 197 + +def self.[](code) + case code + when 404 + NotFoundError + when 401 + UnauthorizedError + when 429 + RateLimitError + else + ApiError + end +end+ |
+
The CDN SimpleClient accesses 'cdn.contentful.com' to get raw JSON responses. It exposes methods to query entries, assets, and content_types. The responses are instances of WCC::Contentful::SimpleClient::Response which handles paging automatically.
+ + +Gets an asset by ID.
+Queries assets with optional query parameters.
+Queries content types with optional query parameters.
+Queries entries with optional query parameters.
+Gets an entry by ID.
+A new instance of Cdn.
+Accesses the Sync API to get a list of items that have changed since the last sync.
+#_instrumentation_event_prefix, instrument
+ +Returns a new instance of Cdn.
+ + +
+ + + +10 +11 +12 +13 +14 +15 +16 +17+ |
+
+ # File 'lib/wcc/contentful/simple_client/cdn.rb', line 10 + +def initialize(space:, access_token:, **) + super( + api_url: [:api_url] || 'https://cdn.contentful.com/', + space: space, + access_token: access_token, + ** + ) +end+ |
+
Gets an asset by ID
+ + +
+ + + +42 +43 +44 +45 +46 +47 +48+ |
+
+ # File 'lib/wcc/contentful/simple_client/cdn.rb', line 42 + +def asset(key, query = {}) + resp = + _instrument 'entries', type: 'Asset', id: key, query: query do + get("assets/#{key}", query) + end + resp.assert_ok! +end+ |
+
Queries assets with optional query parameters
+ + +
+ + + +51 +52 +53 +54 +55 +56 +57+ |
+
+ # File 'lib/wcc/contentful/simple_client/cdn.rb', line 51 + +def assets(query = {}) + resp = + _instrument 'entries', type: 'Asset', query: query do + get('assets', query) + end + resp.assert_ok! +end+ |
+
+ + + +19 +20 +21+ |
+
+ # File 'lib/wcc/contentful/simple_client/cdn.rb', line 19 + +def client_type + 'cdn' +end+ |
+
Queries content types with optional query parameters
+ + +
+ + + +60 +61 +62 +63 +64 +65 +66+ |
+
+ # File 'lib/wcc/contentful/simple_client/cdn.rb', line 60 + +def content_types(query = {}) + resp = + _instrument 'content_types', query: query do + get('content_types', query) + end + resp.assert_ok! +end+ |
+
Queries entries with optional query parameters
+ + +
+ + + +33 +34 +35 +36 +37 +38 +39+ |
+
+ # File 'lib/wcc/contentful/simple_client/cdn.rb', line 33 + +def entries(query = {}) + resp = + _instrument 'entries', type: 'Entry', query: query do + get('entries', query) + end + resp.assert_ok! +end+ |
+
Gets an entry by ID
+ + +
+ + + +24 +25 +26 +27 +28 +29 +30+ |
+
+ # File 'lib/wcc/contentful/simple_client/cdn.rb', line 24 + +def entry(key, query = {}) + resp = + _instrument 'entries', id: key, type: 'Entry', query: query do + get("entries/#{key}", query) + end + resp.assert_ok! +end+ |
+
Accesses the Sync API to get a list of items that have changed since the last sync. Accepts a block that receives each changed item, and returns the next sync token.
+ +If `sync_token` is nil, an initial sync is performed.
+ + +
+ + + +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104+ |
+
+ # File 'lib/wcc/contentful/simple_client/cdn.rb', line 81 + +def sync(sync_token: nil, **query, &block) + return sync_old(sync_token: sync_token, **query) unless block_given? + + sync_token = + if sync_token + { sync_token: sync_token } + else + { initial: true } + end + query = query.merge(sync_token) + + _instrument 'sync', sync_token: sync_token, query: query do + resp = get('sync', query) + resp = SyncResponse.new(resp) + resp.assert_ok! + + resp.each_page do |page| + page.page_items.each(&block) + sync_token = resp.next_sync_token + end + end + + sync_token +end+ |
+
A new instance of Management.
+{ “name”: “My webhook”, “url”: “www.example.com/test”, “topics”: [ “Entry.create”, “ContentType.create”, “*.publish”, “Asset.*” ], “httpBasicUsername”: “yolo”, “httpBasicPassword”: “yolo”, “headers”: [ { “key”: “header1”, “value”: “value1” }, { “key”: “header2”, “value”: “value2” } ] }.
+#_instrumentation_event_prefix, instrument
+ +Returns a new instance of Management.
+ + +
+ + + +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15+ |
+
+ # File 'lib/wcc/contentful/simple_client/management.rb', line 5 + +def initialize(space:, management_token:, **) + super( + **, + api_url: [:management_api_url] || 'https://api.contentful.com', + space: space, + access_token: management_token, + ) + + @post_adapter = @adapter if @adapter.respond_to?(:post) + @post_adapter ||= self.class.load_adapter(nil) +end+ |
+
+ + + +17 +18 +19+ |
+
+ # File 'lib/wcc/contentful/simple_client/management.rb', line 17 + +def client_type + 'management' +end+ |
+
+ + + +29 +30 +31 +32 +33 +34 +35+ |
+
+ # File 'lib/wcc/contentful/simple_client/management.rb', line 29 + +def content_type(key, query = {}) + resp = + _instrument 'content_types', content_type: key, query: query do + get("content_types/#{key}", query) + end + resp.assert_ok! +end+ |
+
+ + + +21 +22 +23 +24 +25 +26 +27+ |
+
+ # File 'lib/wcc/contentful/simple_client/management.rb', line 21 + +def content_types(**query) + resp = + _instrument 'content_types', query: query do + get('content_types', query) + end + resp.assert_ok! +end+ |
+
+ + + +37 +38 +39 +40 +41 +42 +43+ |
+
+ # File 'lib/wcc/contentful/simple_client/management.rb', line 37 + +def editor_interface(content_type_id, query = {}) + resp = + _instrument 'editor_interfaces', content_type: content_type_id, query: query do + get("content_types/#{content_type_id}/editor_interface", query) + end + resp.assert_ok! +end+ |
+
+ + + +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94+ |
+
+ # File 'lib/wcc/contentful/simple_client/management.rb', line 83 + +def post(path, body) + url = URI.join(@api_url, path) + + resp = + _instrument 'post_http', url: url do + post_http(url, body) + end + + Response.new(self, + { url: url, body: body }, + resp) +end+ |
+
{
+ +"name": "My webhook",
+"url": "https://www.example.com/test",
+"topics": [
+ "Entry.create",
+ "ContentType.create",
+ "*.publish",
+ "Asset.*"
+],
+"httpBasicUsername": "yolo",
+"httpBasicPassword": "yolo",
+"headers": [
+ {
+ "key": "header1",
+ "value": "value1"
+ },
+ {
+ "key": "header2",
+ "value": "value2"
+ }
+]
+
+
+}
+ + +
+ + + +75 +76 +77 +78 +79 +80 +81+ |
+
+ # File 'lib/wcc/contentful/simple_client/management.rb', line 75 + +def post_webhook_definition(webhook) + resp = + _instrument 'post.webhook_definitions' do + post("/spaces/#{space}/webhook_definitions", webhook) + end + resp.assert_ok! +end+ |
+
+ + + +45 +46 +47 +48 +49 +50 +51+ |
+
+ # File 'lib/wcc/contentful/simple_client/management.rb', line 45 + +def webhook_definitions(**query) + resp = + _instrument 'webhook_definitions', query: query do + get("/spaces/#{space}/webhook_definitions", query) + end + resp.assert_ok! +end+ |
+
This class inherits a constructor from WCC::Contentful::SimpleClient::ApiError
+ +A new instance of PaginatingEnumerable.
+Returns a new instance of PaginatingEnumerable.
+ + +
+ + + +177 +178 +179 +180 +181+ |
+
+ # File 'lib/wcc/contentful/simple_client/response.rb', line 177 + +def initialize(initial_page) + raise ArgumentError, 'Must provide initial page' unless initial_page.present? + + @initial_page = initial_page +end+ |
+
+ + + +183 +184 +185 +186 +187 +188 +189 +190 +191+ |
+
+ # File 'lib/wcc/contentful/simple_client/response.rb', line 183 + +def each + page = @initial_page + yield page + + while page.next_page? + page = page.next_page + yield page + end +end+ |
+
A new instance of Preview.
+#asset, #assets, #content_types, #entries, #entry, #sync
+ + + + + + + + + +#_instrumentation_event_prefix, instrument
+ +Returns a new instance of Preview.
+ + +
+ + + +5 +6 +7 +8 +9 +10 +11 +12+ |
+
+ # File 'lib/wcc/contentful/simple_client/preview.rb', line 5 + +def initialize(space:, preview_token:, **) + super( + **, + api_url: [:preview_api_url] || 'https://preview.contentful.com/', + space: space, + access_token: preview_token + ) +end+ |
+
+ + + +14 +15 +16+ |
+
+ # File 'lib/wcc/contentful/simple_client/preview.rb', line 14 + +def client_type + 'preview' +end+ |
+
This class inherits a constructor from WCC::Contentful::SimpleClient::ApiError
+ +A new instance of Response.
+#_instrumentation_event_prefix, instrument
+ +Returns a new instance of Response.
+ + +
+ + + +61 +62 +63 +64 +65 +66+ |
+
+ # File 'lib/wcc/contentful/simple_client/response.rb', line 61 + +def initialize(client, request, raw_response) + @client = client + @request = request + @raw_response = raw_response + @body = raw_response.body.to_s +end+ |
+
+ + + +9 +10 +11+ |
+
+ # File 'lib/wcc/contentful/simple_client/response.rb', line 9 + +def client + @client +end+ |
+
+ + + +9 +10 +11+ |
+
+ # File 'lib/wcc/contentful/simple_client/response.rb', line 9 + +def raw_response + @raw_response +end+ |
+
+ + + +9 +10 +11+ |
+
+ # File 'lib/wcc/contentful/simple_client/response.rb', line 9 + +def request + @request +end+ |
+
+ + + +68 +69 +70 +71 +72+ |
+
+ # File 'lib/wcc/contentful/simple_client/response.rb', line 68 + +def assert_ok! + return self if status >= 200 && status < 300 + + raise ApiError[status], self +end+ |
+
+ + + +15 +16 +17+ |
+
+ # File 'lib/wcc/contentful/simple_client/response.rb', line 15 + +def body + @body ||= raw_response.body.to_s +end+ |
+
+ + + +94 +95 +96+ |
+
+ # File 'lib/wcc/contentful/simple_client/response.rb', line 94 + +def count + total +end+ |
+
+ + + +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84+ |
+
+ # File 'lib/wcc/contentful/simple_client/response.rb', line 74 + +def each_page(&block) + raise ArgumentError, 'Not a collection response' unless page_items + + ret = PaginatingEnumerable.new(self) + + if block_given? + ret.map(&block) + else + ret.lazy + end +end+ |
+
+ + + +24 +25 +26 +27 +28 +29 +30 +31 +32+ |
+
+ # File 'lib/wcc/contentful/simple_client/response.rb', line 24 + +def + = + begin + raw['message'] + rescue JSON::ParserError + nil + end + || "#{code}: #{raw_response.body}" +end+ |
+
+ + + +98 +99 +100 +101 +102+ |
+
+ # File 'lib/wcc/contentful/simple_client/response.rb', line 98 + +def first + raise ArgumentError, 'Not a collection response' unless page_items + + page_items.first +end+ |
+
+ + + +104 +105 +106 +107 +108 +109+ |
+
+ # File 'lib/wcc/contentful/simple_client/response.rb', line 104 + +def includes + @includes ||= + raw['includes']&.each_with_object({}) do |(_t, entries), h| + entries.each { |e| h[e.dig('sys', 'id')] = e } + end || {} +end+ |
+
+ + + +86 +87 +88+ |
+
+ # File 'lib/wcc/contentful/simple_client/response.rb', line 86 + +def items + each_page.flat_map(&:page_items) +end+ |
+
+ + + +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59+ |
+
+ # File 'lib/wcc/contentful/simple_client/response.rb', line 48 + +def next_page + return unless next_page? + + query = (@request[:query] || {}).merge({ + skip: page_items.length + skip + }) + np = + _instrument 'page', url: @request[:url], query: query do + @client.get(@request[:url], query) + end + np.assert_ok! +end+ |
+
+ + + +42 +43 +44 +45 +46+ |
+
+ # File 'lib/wcc/contentful/simple_client/response.rb', line 42 + +def next_page? + return unless raw.key? 'items' + + page_items.length + skip < total +end+ |
+
+ + + +90 +91 +92+ |
+
+ # File 'lib/wcc/contentful/simple_client/response.rb', line 90 + +def page_items + raw['items'] +end+ |
+
+ + + +19 +20 +21+ |
+
+ # File 'lib/wcc/contentful/simple_client/response.rb', line 19 + +def raw + @raw ||= JSON.parse(body) +end+ |
+
+ + + +34 +35 +36+ |
+
+ # File 'lib/wcc/contentful/simple_client/response.rb', line 34 + +def skip + raw['skip'] +end+ |
+
+ + + +38 +39 +40+ |
+
+ # File 'lib/wcc/contentful/simple_client/response.rb', line 38 + +def total + raw['total'] +end+ |
+
#client, #raw_response, #request
+ + + +A new instance of SyncResponse.
+#assert_ok!, #body, #error_message, #first, #includes, #items, #page_items, #raw, #skip, #total
+ + + + + + + + + +#_instrumentation_event_prefix, instrument
+ +Returns a new instance of SyncResponse.
+ + +
+ + + +113 +114 +115 +116+ |
+
+ # File 'lib/wcc/contentful/simple_client/response.rb', line 113 + +def initialize(response, memoize: false) + super(response.client, response.request, response.raw_response) + @memoize = memoize +end+ |
+
+ + + +167 +168 +169 +170 +171+ |
+
+ # File 'lib/wcc/contentful/simple_client/response.rb', line 167 + +def self.parse_sync_token(url) + url = URI.parse(url) + q = CGI.parse(url.query) + q['sync_token']&.first +end+ |
+
+ + + +162 +163 +164 +165+ |
+
+ # File 'lib/wcc/contentful/simple_client/response.rb', line 162 + +def count + raise NotImplementedError, + 'Sync does not return an accurate total. Use #items.count instead.' +end+ |
+
+ + + +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160+ |
+
+ # File 'lib/wcc/contentful/simple_client/response.rb', line 147 + +def each_page(&block) + if block_given? + super do |page| + @last_sync_token = page.next_sync_token + + yield page + end + else + super.map do |page| + @last_sync_token = page.next_sync_token + page + end + end +end+ |
+
+ + + +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136+ |
+
+ # File 'lib/wcc/contentful/simple_client/response.rb', line 122 + +def next_page + return unless next_page? + return @next_page if @next_page + + url = raw['nextPageUrl'] + next_page = + _instrument 'page', url: url do + @client.get(url) + end + + next_page = SyncResponse.new(next_page) + next_page.assert_ok! + @next_page = next_page if @memoize + next_page +end+ |
+
+ + + +118 +119 +120+ |
+
+ # File 'lib/wcc/contentful/simple_client/response.rb', line 118 + +def next_page? + raw['nextPageUrl'].present? +end+ |
+
+ + + +138 +139 +140 +141 +142 +143 +144 +145+ |
+
+ # File 'lib/wcc/contentful/simple_client/response.rb', line 138 + +def next_sync_token + # If we have iterated some pages, return the sync token of the final + # page that was iterated. Do this without maintaining a reference to + # all the pages. + return @last_sync_token if @last_sync_token + + SyncResponse.parse_sync_token(raw['nextPageUrl'] || raw['nextSyncUrl']) +end+ |
+
+ + + + + Classes: Response + + +
+ + + + + + + + +
+ + + +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17+ |
+
+ # File 'lib/wcc/contentful/simple_client/typhoeus_adapter.rb', line 7 + +def get(url, params = {}, headers = {}) + req = OpenStruct.new(params: params, headers: headers) + yield req if block_given? + Response.new( + Typhoeus.get( + url, + params: req.params, + headers: req.headers + ) + ) +end+ |
+
+ + + +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29+ |
+
+ # File 'lib/wcc/contentful/simple_client/typhoeus_adapter.rb', line 19 + +def post(url, body, headers = {}, proxy = {}) + raise NotImplementedError, 'Proxying Not Yet Implemented' if proxy[:host] + + Response.new( + Typhoeus.post( + url, + body: body.to_json, + headers: headers + ) + ) +end+ |
+
+ + + +34 +35 +36+ |
+
+ # File 'lib/wcc/contentful/simple_client/typhoeus_adapter.rb', line 34 + +def raw + __getobj__ +end+ |
+
+ + + +38 +39 +40+ |
+
+ # File 'lib/wcc/contentful/simple_client/typhoeus_adapter.rb', line 38 + +def status + code +end+ |
+
This class inherits a constructor from WCC::Contentful::SimpleClient::ApiError
+ +The “Store” is the middle layer in the WCC::Contentful gem. It exposes an API that implements the configured content delivery strategy.
+ +The different content delivery strategies require different store implementations.
+Uses the WCC::Contentful::Store::CDNAdapter to wrap the Contentful CDN, providing an API consistent with the other stores. Any query made to the CDNAdapter will be immediately passed through to the API. The CDNAdapter does not implement #index because it does not care about updates coming from the Sync API.
+Uses the Contentful CDN in combination with an ActiveSupport::Cache implementation in order to respond with the cached data where possible, saving your CDN quota. The cache is kept up-to-date via the Sync Engine and the WCC::Contentful::SyncEngine::Job. It is correct, but not complete.
+Uses one of the full store implementations to store the entirety of the Contentful space locally. All queries are run against this local copy, which is kept up to date via the Sync Engine and the WCC::Contentful::SyncEngine::Job. The local store is correct and complete.
+The currently configured store is available on WCC::Contentful::Services.instance.store
+ + ++ + + Modules: Instrumentation, InstrumentationWrapper, Interface + + + + Classes: Base, CDNAdapter, Factory, InstrumentationMiddleware, MemoryStore, PostgresStore, Query + + +
+ + +{ + memory: ->(_config, *) { WCC::Contentful::Store::MemoryStore.new }, + postgres: ->(config, *) { + require_relative 'store/postgres_store' + WCC::Contentful::Store::PostgresStore.new(config, *) + } +}.freeze
%i[ + eager_sync + lazy_sync + direct + custom +].freeze
At a minimum subclasses should override Interface#find, #execute, #set, and ##delete. As an alternative to overriding set and delete, the subclass can override #index. Index is called when a webhook triggers a sync, to update the store.
+This is the base class for stores which implement #index, and therefore must be kept up-to-date via the Sync API. To implement a new store, you should include the rspec_examples in your rspec tests for the store. See spec/wcc/contentful/store/memory_store_spec.rb for an example.
+ + +Removes the entry by ID from the store.
+Finds all entries of the given content type.
+Finds the first entry matching the given filter.
+Processes a data point received via the Sync API.
+Returns true if this store can persist entries and assets which are retrieved from the sync API.
+Sets the value of the entry with the given ID in the store.
+Removes the entry by ID from the store.
+ + +
+ + + +28 +29 +30+ |
+
+ # File 'lib/wcc/contentful/store/base.rb', line 28 + +def delete(_id) + raise NotImplementedError, "#{self.class} does not implement #delete" +end+ |
+
+ + + +110 +111 +112+ |
+
+ # File 'lib/wcc/contentful/store/base.rb', line 110 + +def ensure_hash(val) + raise ArgumentError, 'Value must be a Hash' unless val.is_a?(Hash) +end+ |
+
Executes a WCC::Contentful::Store::Query object created by #find_all or #find_by. Implementations should override this to translate the query's conditions into a query against the datastore.
+ +For a very naiive implementation see WCC::Contentful::Store::MemoryStore#execute
+ + +
+ + + +38 +39 +40+ |
+
+ # File 'lib/wcc/contentful/store/base.rb', line 38 + +def execute(_query) + raise NotImplementedError, "#{self.class} does not implement #execute" +end+ |
+
Finds all entries of the given content type. A content type is required.
+ +Subclasses may override this to provide their own query implementation,
+ +or else override #execute to run the query after it has been parsed.
+
+
+
+
+ + + +102 +103 +104 +105 +106 +107 +108+ |
+
+ # File 'lib/wcc/contentful/store/base.rb', line 102 + +def find_all(content_type:, options: nil) + Query.new( + self, + content_type: content_type, + options: + ) +end+ |
+
Finds the first entry matching the given filter. A content type is required.
+ + +
+ + + +87 +88 +89 +90 +91 +92+ |
+
+ # File 'lib/wcc/contentful/store/base.rb', line 87 + +def find_by(content_type:, filter: nil, options: nil) + # default implementation - can be overridden + q = find_all(content_type: content_type, options: { limit: 1 }.merge!( || {})) + q = q.apply(filter) if filter + q.first +end+ |
+
Processes a data point received via the Sync API. This can be a published entry or asset, or a 'DeletedEntry' or 'DeletedAsset'. The default implementation calls into #set and #delete to perform the appropriate operations in the store.
+ + +
+ + + +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78+ |
+
+ # File 'lib/wcc/contentful/store/base.rb', line 52 + +def index(json) + # This implementation assumes that #delete and #set are individually thread-safe. + # No mutex is needed so long as the revisions are accurate. + # Subclasses can override to do this in a more performant thread-safe way. + # Example: postgres_store could do this in a stored procedure for speed + prev = + case type = json.dig('sys', 'type') + when 'DeletedEntry', 'DeletedAsset' + delete(json.dig('sys', 'id')) + else + set(json.dig('sys', 'id'), json) + end + + if (prev_rev = prev&.dig('sys', 'revision')) && + (next_rev = json.dig('sys', 'revision')) && + (next_rev < prev_rev) + # Uh oh! we overwrote an entry with a prior revision. Put the previous back. + return index(prev) + end + + case type + when 'DeletedEntry', 'DeletedAsset' + nil + else + json + end +end+ |
+
Returns true if this store can persist entries and assets which are retrieved from the sync API.
+ + +
+ + + +44 +45 +46+ |
+
+ # File 'lib/wcc/contentful/store/base.rb', line 44 + +def index? + true +end+ |
+
Sets the value of the entry with the given ID in the store.
+ + +
+ + + +22 +23 +24+ |
+
+ # File 'lib/wcc/contentful/store/base.rb', line 22 + +def set(_id, _value) + raise NotImplementedError, "#{self.class} does not implement #set" +end+ |
+
+ + + + + Classes: Query + + +
+ + + +NOTE: CDNAdapter should not instrument store events cause it's not a store.
+The CDNAdapter cannot index data coming back from the Sync API.
+Intentionally not implementing write methods.
+Intentionally not implementing write methods
+ + +
+ + + +25 +26 +27 +28 +29+ |
+
+ # File 'lib/wcc/contentful/store/cdn_adapter.rb', line 25 + +def initialize(client = nil, preview: false) + super() + @client = client + @preview = preview +end+ |
+
+ + + +10 +11 +12+ |
+
+ # File 'lib/wcc/contentful/store/cdn_adapter.rb', line 10 + +def client + @preview ? @preview_client : @client +end+ |
+
NOTE: CDNAdapter should not instrument store events cause it's not a store.
+ + +
+ + + +8 +9 +10+ |
+
+ # File 'lib/wcc/contentful/store/cdn_adapter.rb', line 8 + +def preview_client=(value) + @preview_client = value +end+ |
+
+ + + +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46+ |
+
+ # File 'lib/wcc/contentful/store/cdn_adapter.rb', line 31 + +def find(key, hint: nil, **) + = { locale: '*' }.merge!( || {}) + entry = + if hint + client.public_send(hint.underscore, key, ) + else + begin + client.entry(key, ) + rescue WCC::Contentful::SimpleClient::NotFoundError + client.asset(key, ) + end + end + entry&.raw +rescue WCC::Contentful::SimpleClient::NotFoundError + nil +end+ |
+
+ + + +55 +56 +57 +58 +59 +60 +61 +62+ |
+
+ # File 'lib/wcc/contentful/store/cdn_adapter.rb', line 55 + +def find_all(content_type:, options: nil) + Query.new( + self, + client: client, + relation: { content_type: content_type }, + options: + ) +end+ |
+
+ + + +48 +49 +50 +51 +52 +53+ |
+
+ # File 'lib/wcc/contentful/store/cdn_adapter.rb', line 48 + +def find_by(content_type:, filter: nil, options: nil) + # default implementation - can be overridden + q = find_all(content_type: content_type, options: { limit: 1 }.merge!( || {})) + q = q.apply(filter) if filter + q.first +end+ |
+
+ + + +19 +20 +21+ |
+
+ # File 'lib/wcc/contentful/store/cdn_adapter.rb', line 19 + +def index + raise NotImplementedError, 'Cannot put data to the CDN!' +end+ |
+
The CDNAdapter cannot index data coming back from the Sync API.
+ + +
+ + + +15 +16 +17+ |
+
+ # File 'lib/wcc/contentful/store/cdn_adapter.rb', line 15 + +def index? + false +end+ |
+
Called with a filter object by Base#find_by in order to apply the filter.
+A new instance of Query.
+Returns a new instance of Query.
+ + +
+ + + +84 +85 +86 +87 +88 +89 +90 +91 +92 +93+ |
+
+ # File 'lib/wcc/contentful/store/cdn_adapter.rb', line 84 + +def initialize(store, client:, relation:, options: nil, **extra) + raise ArgumentError, 'Client cannot be nil' unless client.present? + raise ArgumentError, 'content_type must be provided' unless relation[:content_type].present? + + @store = store + @client = client + @relation = relation + @options = || {} + @extra = extra || {} +end+ |
+
Called with a filter object by Base#find_by in order to apply the filter.
+ + +
+ + + +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108+ |
+
+ # File 'lib/wcc/contentful/store/cdn_adapter.rb', line 96 + +def apply(filter, context = nil) + filter.reduce(self) do |query, (field, value)| + if value.is_a?(Hash) + if op?(k = value.keys.first) + query.apply_operator(k.to_sym, field.to_s, value[k], context) + else + query.nested_conditions(field, value, context) + end + else + query.apply_operator(:eq, field.to_s, value) + end + end +end+ |
+
+ + + +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126+ |
+
+ # File 'lib/wcc/contentful/store/cdn_adapter.rb', line 110 + +def apply_operator(operator, field, expected, context = nil) + op = operator == :eq ? nil : operator + if expected.is_a?(Array) + expected = expected.join(',') + op = :in if op.nil? + end + + param = parameter(field, operator: op, context: context, locale: true) + + self.class.new( + @store, + client: @client, + relation: @relation.merge(param => expected), + options: @options, + **@extra + ) +end+ |
+
+ + + +128 +129 +130 +131 +132 +133 +134+ |
+
+ # File 'lib/wcc/contentful/store/cdn_adapter.rb', line 128 + +def nested_conditions(field, conditions, context) + base_param = parameter(field) + + conditions.reduce(self) do |query, (ref, value)| + query.apply({ "#{base_param}.#{parameter(ref)}" => value }, context) + end +end+ |
+
+ + + +74 +75 +76 +77 +78 +79 +80 +81 +82+ |
+
+ # File 'lib/wcc/contentful/store/cdn_adapter.rb', line 74 + +def to_enum + return response.each_page.flat_map(&:page_items) unless @options[:include] + + response.each_page + .flat_map { |page| page.page_items.each_with_object(page).to_a } + .map do |e, page| + resolve_includes(e, page.includes, depth: @options[:include]) + end +end+ |
+
This factory presents a DSL for configuring the store stack. The store stack sits in between the Model layer and the datastore, which can be Contentful or something else like Postgres.
+ +A set of “presets” are available to get pre-configured stacks based on what we've found most useful.
+ + +Set the base store instance.
+The middleware that by default lives at the top of the middleware stack.
+A new instance of Factory.
+An array of tuples that set up and configure a Store middleware.
+Configures the default “direct” preset which passes everything through to Contentful CDN.
+Sets the “eager sync” preset using one of the preregistered stores like :postgres.
+Configures a “lazy sync” preset which caches direct lookups but hits Contentful for any missing information.
+Adds a middleware to the chain.
+Returns a new instance of Factory.
+ + +
+ + + +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40+ |
+
+ # File 'lib/wcc/contentful/store/factory.rb', line 27 + +def initialize(config = WCC::Contentful.configuration, preset = :direct, = nil) + @config = config + @preset = preset || :custom + @options = [*] || [] + + # Infer whether they passed in a store implementation object or class + if class_implements_store_interface?(@preset) || + object_implements_store_interface?(@preset) + @options.unshift(@preset) + @preset = :custom + end + + configure_preset(@preset) +end+ |
+
+ + + +17 +18 +19+ |
+
+ # File 'lib/wcc/contentful/store/factory.rb', line 17 + +def config + @config +end+ |
+
+ + + +17 +18 +19+ |
+
+ # File 'lib/wcc/contentful/store/factory.rb', line 17 + +def + @options +end+ |
+
+ + + +17 +18 +19+ |
+
+ # File 'lib/wcc/contentful/store/factory.rb', line 17 + +def preset + @preset +end+ |
+
Set the base store instance.
+ + +
+ + + +20 +21 +22+ |
+
+ # File 'lib/wcc/contentful/store/factory.rb', line 20 + +def store + @store +end+ |
+
The middleware that by default lives at the top of the middleware stack.
+ + +
+ + + +175 +176 +177 +178 +179+ |
+
+ # File 'lib/wcc/contentful/store/factory.rb', line 175 + +def default_middleware + [ + [WCC::Contentful::Store::InstrumentationMiddleware] + ].freeze +end+ |
+
+ + + +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82+ |
+
+ # File 'lib/wcc/contentful/store/factory.rb', line 64 + +def build(services = WCC::Contentful::Services.instance) + store_instance = build_store(services) + = { + config: config, + services: services + } + middleware.reverse + .reduce(store_instance) do |memo, middleware_config| + # May have added a middleware with `middleware << MyMiddleware.new` + middleware_config = [middleware_config] unless middleware_config.is_a? Array + + middleware, params, configure_proc = middleware_config + = .merge((params || []).) + middleware = middleware.call(memo, *params, **) + services.inject_into(middleware, except: %i[store preview_store]) + middleware&.instance_exec(&configure_proc) if configure_proc + middleware || memo + end +end+ |
+
An array of tuples that set up and configure a Store middleware.
+ + +
+ + + +23 +24 +25+ |
+
+ # File 'lib/wcc/contentful/store/factory.rb', line 23 + +def middleware + @middleware ||= self.class.default_middleware.dup +end+ |
+
+ + + +120 +121 +122+ |
+
+ # File 'lib/wcc/contentful/store/factory.rb', line 120 + +def preset_custom + self.store = .shift +end+ |
+
Configures the default “direct” preset which passes everything through to Contentful CDN
+ + +
+ + + +116 +117 +118+ |
+
+ # File 'lib/wcc/contentful/store/factory.rb', line 116 + +def preset_direct + self.store = CDNAdapter.new(preview: .include?(:preview)) +end+ |
+
Sets the “eager sync” preset using one of the preregistered stores like :postgres
+ + +
+ + + +100 +101 +102 +103 +104+ |
+
+ # File 'lib/wcc/contentful/store/factory.rb', line 100 + +def preset_eager_sync + store = .shift || :memory + store = SYNC_STORES[store]&.call(config, *) if store.is_a?(Symbol) + self.store = store +end+ |
+
Configures a “lazy sync” preset which caches direct lookups but hits Contentful for any missing information. The cache is kept up to date by the sync engine.
+ + +
+ + + +108 +109 +110 +111 +112+ |
+
+ # File 'lib/wcc/contentful/store/factory.rb', line 108 + +def preset_lazy_sync + preset_direct + use(WCC::Contentful::Middleware::Store::CachingMiddleware, + ActiveSupport::Cache.lookup_store(*)) +end+ |
+
+ + + +49 +50 +51 +52 +53 +54 +55+ |
+
+ # File 'lib/wcc/contentful/store/factory.rb', line 49 + +def replace(middleware, *middleware_params, &block) + idx = self.middleware.find_index { |m| m[0] == middleware } + raise ArgumentError, "Middleware #{middleware} not present" if idx.nil? + + configure_proc = block_given? ? Proc.new(&block) : nil + self.middleware[idx] = [middleware, middleware_params, configure_proc] +end+ |
+
+ + + +57 +58 +59 +60 +61 +62+ |
+
+ # File 'lib/wcc/contentful/store/factory.rb', line 57 + +def unuse(middleware) + idx = self.middleware.find_index { |m| m[0] == middleware } + return if idx.nil? + + self.middleware.delete_at idx +end+ |
+
Adds a middleware to the chain. Use a block here to configure the middleware after it has been created.
+ + +
+ + + +44 +45 +46 +47+ |
+
+ # File 'lib/wcc/contentful/store/factory.rb', line 44 + +def use(middleware, *middleware_params, &block) + configure_proc = block_given? ? Proc.new(&block) : nil + self.middleware << [middleware, middleware_params, configure_proc] +end+ |
+
+ + + +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97+ |
+
+ # File 'lib/wcc/contentful/store/factory.rb', line 84 + +def validate! + unless preset.nil? || PRESETS.include?(preset) + raise ArgumentError, "Please use one of #{PRESETS} instead of #{preset}" + end + + middleware.each do |m| + next if m[0].respond_to?(:call) + + raise ArgumentError, "The middleware '#{m[0]&.try(:name) || m[0]}' cannot be applied! " \ + 'It must respond to :call' + end + + validate_store!(store) +end+ |
+
WCC::Contentful::Store::Interface::INTERFACE_METHODS
+ + + + +#find, #find_all, #find_by, #has_select?, #resolve_includes, #resolve_link, #resolved_link?, #transform
+ + + + + + + + + + +
+ + + +22 +23 +24 +25 +26+ |
+
+ # File 'lib/wcc/contentful/store/instrumentation.rb', line 22 + +def find(key, **) + _instrument 'find', id: key, options: do + super + end +end+ |
+
+ + + +40 +41 +42 +43 +44+ |
+
+ # File 'lib/wcc/contentful/store/instrumentation.rb', line 40 + +def find_all(**params) + # end happens when query is executed - todo. + _instrument 'find_all', params.slice(:content_type, :options) + super +end+ |
+
+ + + +34 +35 +36 +37 +38+ |
+
+ # File 'lib/wcc/contentful/store/instrumentation.rb', line 34 + +def find_by(**params) + _instrument 'find_by', params.slice(:content_type, :filter, :options) do + super + end +end+ |
+
+ + + +28 +29 +30 +31 +32+ |
+
+ # File 'lib/wcc/contentful/store/instrumentation.rb', line 28 + +def index(json) + _instrument 'index', id: json.dig('sys', 'id') do + super + end +end+ |
+
This module represents the common interface of all Store implementations. It is documentation ONLY and does not add functionality.
+ +This is distinct from WCC::Contentful::Store::Base, because certain helpers exposed publicly by that abstract class are not part of the actual interface and can change without a major version update. rubocop:disable Lint/UnusedMethodArgument
+ + +WCC::Contentful::Store::Interface.instance_methods - Module.instance_methods
Finds an entry by it's ID.
+Finds all entries of the given content type.
+Finds the first entry matching the given filter.
+Processes a data point received via the Sync API.
+Returns true if this store can persist entries and assets which are retrieved from the sync API.
+Subclasses should implement this at a minimum to provide data to the WCC::Contentful::Model API.
+Finds an entry by it's ID. The returned entry is a JSON hash sig String).returns(T.any(Entry, Asset))
+ + +
+ + + +40 +41 +42+ |
+
+ # File 'lib/wcc/contentful/store/interface.rb', line 40 + +def find(_id) + raise NotImplementedError, "#{self.class} does not implement #find" +end+ |
+
Finds all entries of the given content type. A content type is required.
+ +Subclasses may override this to provide their own query implementation,
+ +or else override #execute to run the query after it has been parsed.
+
+
+sig +
content_type: String,
+filter: T.nilable(T::Hash[T.any(Symbol, String), T.untyped]),
+options: T.nilable(T::Hash[T.any(Symbol), T.untyped]),
+
+
+).returns(WCC::Contentful::Store::Query::Interface)
+ + +
+ + + +75 +76 +77+ |
+
+ # File 'lib/wcc/contentful/store/interface.rb', line 75 + +def find_all(content_type:, options: nil) + raise NotImplementedError, "#{self.class} does not implement #find_all" +end+ |
+
Finds the first entry matching the given filter. A content type is required.
+ +sig +
content_type: String,
+filter: T.nilable(T::Hash[T.any(Symbol, String), T.untyped]),
+options: T.nilable(T::Hash[T.any(Symbol), T.untyped]),
+
+
+).returns(T.any(Entry, Asset))
+ + +
+ + + +56 +57 +58+ |
+
+ # File 'lib/wcc/contentful/store/interface.rb', line 56 + +def find_by(content_type:, filter: nil, options: nil) + raise NotImplementedError, "#{self.class} does not implement #find_by" +end+ |
+
Processes a data point received via the Sync API. This can be a published entry or asset, or a 'DeletedEntry' or 'DeletedAsset'. The default implementation calls into #set and #delete to perform the appropriate operations in the store. sig T.any(Entry, Asset, DeletedEntry, DeletedAsset))
+ +.returns(T.any(Entry, Asset, nil))
+
+
+
+
+ + + +32 +33 +34+ |
+
+ # File 'lib/wcc/contentful/store/interface.rb', line 32 + +def index(_json) + raise NotImplementedError, "#{self.class} does not implement #index" +end+ |
+
Returns true if this store can persist entries and assets which are retrieved from the sync API. sig WCC::Contentful::Store::Interface.abstractabstract.returns(Tabstract.returns(T::Boolean)
+ + +
+ + + +22 +23 +24+ |
+
+ # File 'lib/wcc/contentful/store/interface.rb', line 22 + +def index? + raise NotImplementedError, "#{self.class} does not implement #index?" +end+ |
+
The MemoryStore is the most naiive store implementation and a good reference point for more useful implementations. It only implements equality queries and does not support querying through an association.
+ + +%i[eq ne in nin].freeze
A new instance of MemoryStore.
+#ensure_hash, #find_all, #find_by, #index, #index?
+ + + + + + + + + +#find_all, #find_by, #index, #index?
+Returns a new instance of MemoryStore.
+ + +
+ + + +10 +11 +12 +13 +14 +15+ |
+
+ # File 'lib/wcc/contentful/store/memory_store.rb', line 10 + +def initialize + super + + @mutex = Concurrent::ReentrantReadWriteLock.new + @hash = {} +end+ |
+
+ + + +27 +28 +29 +30 +31+ |
+
+ # File 'lib/wcc/contentful/store/memory_store.rb', line 27 + +def delete(key) + @mutex.with_write_lock do + @hash.delete(key) + end +end+ |
+
+ + + +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70+ |
+
+ # File 'lib/wcc/contentful/store/memory_store.rb', line 45 + +def execute(query) + if bad_op = (query.conditions.map(&:op) - SUPPORTED_OPS).first + raise ArgumentError, "Operator :#{bad_op} not supported" + end + + # Since @hash.values returns a new array, we only need to lock here + relation = @mutex.with_read_lock { @hash.values } + + # relation is an enumerable that we apply conditions to in the form of + # Enumerable#select and Enumerable#reject. + relation = + relation.lazy.reject do |v| + value_content_type = v.try(:dig, 'sys', 'contentType', 'sys', 'id') + if query.content_type == 'Asset' + !value_content_type.nil? + else + value_content_type != query.content_type + end + end + + # For each condition, we apply a new Enumerable#select with a block that + # enforces the condition. + query.conditions.reduce(relation) do |memo, condition| + __send__("apply_#{condition.op}", memo, condition) + end +end+ |
+
+ + + +37 +38 +39 +40 +41+ |
+
+ # File 'lib/wcc/contentful/store/memory_store.rb', line 37 + +def find(key, **) + @mutex.with_read_lock do + @hash[key] + end +end+ |
+
+ + + +33 +34 +35+ |
+
+ # File 'lib/wcc/contentful/store/memory_store.rb', line 33 + +def keys + @mutex.with_read_lock { @hash.keys } +end+ |
+
+ + + +17 +18 +19 +20 +21 +22 +23 +24 +25+ |
+
+ # File 'lib/wcc/contentful/store/memory_store.rb', line 17 + +def set(key, value) + value = value.deep_dup.freeze + ensure_hash value + @mutex.with_write_lock do + old = @hash[key] + @hash[key] = value + old + end +end+ |
+
Implements the store interface where all Contentful entries are stored in a JSONB table.
+ + ++ + + + + Classes: Query + + +
+ + +This is intentionally a class var so that all subclasses share the same mutex
+ + +Mutex.new
rubocop:disable Style/ClassVars.
+A new instance of PostgresStore.
+#_instrumentation_event_prefix, instrument
+ + + + + + + + + + +#ensure_hash, #execute, #find_by, #index, #index?
+ + + + + + + + + +Returns a new instance of PostgresStore.
+ + +
+ + + +20 +21 +22 +23 +24 +25 +26 +27 +28+ |
+
+ # File 'lib/wcc/contentful/store/postgres_store.rb', line 20 + +def initialize(_config = nil, = nil, = nil) + super() + @schema_ensured = false + ||= { dbname: 'postgres' } + ||= {} + @connection_pool = PostgresStore.build_connection_pool(, ) + @dirty = Concurrent::AtomicBoolean.new + @mutex = Mutex.new +end+ |
+
+ + + +17 +18 +19+ |
+
+ # File 'lib/wcc/contentful/store/postgres_store.rb', line 17 + +def connection_pool + @connection_pool +end+ |
+
+ + + +18 +19 +20+ |
+
+ # File 'lib/wcc/contentful/store/postgres_store.rb', line 18 + +def logger + @logger +end+ |
+
rubocop:disable Style/ClassVars
+ + +
+ + + +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336+ |
+
+ # File 'lib/wcc/contentful/store/postgres_store.rb', line 325 + +def build_connection_pool(, ) + ConnectionPool.new() do + PG.connect().tap do |conn| + unless schema_ensured?(conn) + @@schema_mutex.synchronize do + ensure_schema(conn) unless schema_ensured?(conn) + end + end + prepare_statements(conn) + end + end +end+ |
+
+ + + +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362+ |
+
+ # File 'lib/wcc/contentful/store/postgres_store.rb', line 349 + +def ensure_schema(conn) + result = + begin + conn.exec('SELECT version FROM wcc_contentful_schema_version ' \ + 'ORDER BY version DESC') + rescue PG::UndefinedTable + [] + end + 1.upto(EXPECTED_VERSION).each do |version_num| + next if result.find { |row| row['version'].to_s == version_num.to_s } + + conn.exec(File.read(File.join(__dir__, "postgres_store/schema_#{version_num}.sql"))) + end +end+ |
+
+ + + +313 +314 +315 +316 +317 +318 +319 +320+ |
+
+ # File 'lib/wcc/contentful/store/postgres_store.rb', line 313 + +def prepare_statements(conn) + conn.prepare('upsert_entry', 'SELECT * FROM fn_contentful_upsert_entry($1,$2,$3)') + conn.prepare('select_entry', 'SELECT * FROM contentful_raw WHERE id = $1') + conn.prepare('select_ids', 'SELECT id FROM contentful_raw') + conn.prepare('delete_by_id', 'DELETE FROM contentful_raw WHERE id = $1 RETURNING *') + conn.prepare('refresh_views_concurrently', + 'REFRESH MATERIALIZED VIEW CONCURRENTLY contentful_raw_includes_ids_jointable') +end+ |
+
+ + + +338 +339 +340 +341 +342 +343 +344 +345 +346 +347+ |
+
+ # File 'lib/wcc/contentful/store/postgres_store.rb', line 338 + +def schema_ensured?(conn) + result = conn.exec('SELECT version FROM wcc_contentful_schema_version ' \ + 'ORDER BY version DESC LIMIT 1') + return false if result.num_tuples == 0 + + result[0]['version'].to_i >= EXPECTED_VERSION +rescue PG::UndefinedTable + # need to run v1 schema migration + false +end+ |
+
+ + + +76 +77 +78 +79 +80 +81 +82 +83 +84 +85+ |
+
+ # File 'lib/wcc/contentful/store/postgres_store.rb', line 76 + +def delete(key) + result = + _instrument 'delete_by_id', key: key do + @connection_pool.with { |conn| conn.exec_prepared('delete_by_id', [key]) } + end + + return if result.num_tuples == 0 + + JSON.parse(result.getvalue(0, 1)) +end+ |
+
+ + + +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126+ |
+
+ # File 'lib/wcc/contentful/store/postgres_store.rb', line 107 + +def exec_query(statement, params = []) + if @dirty.true? + # Only one thread should call refresh_views_concurrently but all should wait for it to finish. + @mutex.synchronize do + # We have to check again b/c another thread may have gotten here first + if @dirty.true? + _instrument 'refresh_views' do + @connection_pool.with { |conn| conn.exec_prepared('refresh_views_concurrently') } + end + # Mark that the views have been refreshed. + @dirty.make_false + end + end + end + + logger&.debug("[PostgresStore] #{statement} #{params.inspect}") + _instrument 'exec' do + @connection_pool.with { |conn| conn.exec(statement, params) } + end +end+ |
+
+ + + +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97+ |
+
+ # File 'lib/wcc/contentful/store/postgres_store.rb', line 87 + +def find(key, **) + result = + _instrument 'select_entry', key: key do + @connection_pool.with { |conn| conn.exec_prepared('select_entry', [key]) } + end + return if result.num_tuples == 0 + + JSON.parse(result.getvalue(0, 1)) +rescue PG::ConnectionBad + nil +end+ |
+
+ + + +99 +100 +101 +102 +103 +104 +105+ |
+
+ # File 'lib/wcc/contentful/store/postgres_store.rb', line 99 + +def find_all(content_type:, options: nil) + Query.new( + self, + content_type: content_type, + options: + ) +end+ |
+
+ + + +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74+ |
+
+ # File 'lib/wcc/contentful/store/postgres_store.rb', line 63 + +def keys + result = + _instrument 'select_ids' do + @connection_pool.with { |conn| conn.exec_prepared('select_ids') } + end + + arr = [] + result.each { |r| arr << r['id'].strip } + arr +rescue PG::ConnectionBad + [] +end+ |
+
+ + + +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61+ |
+
+ # File 'lib/wcc/contentful/store/postgres_store.rb', line 30 + +def set(key, value) + ensure_hash value + + result = + _instrument 'upsert_entry' do + @connection_pool.with do |conn| + conn.exec_prepared('upsert_entry', [ + key, + value.to_json, + quote_array(extract_links(value)) + ]) + end + end + + previous_value = + if result.num_tuples == 0 + nil + else + val = result.getvalue(0, 0) + JSON.parse(val) if val + end + + if views_need_update?(value, previous_value) + # Mark the views as needing to be refreshed, they will be refreshed on the next query. + was_dirty = @dirty.make_true + # Send out an instrumentation event if we are the thread that marked it dirty + # (make_true returns true if the value changed) + _instrument 'mark_dirty' if was_dirty + end + + previous_value +end+ |
+
Query::FALSE_VALUES, Query::RESERVED_NAMES
+ + + +#conditions, #content_type, #store
+ + + +#apply, #apply_operator, flatten_filter_hash, #initialize, known_locales, normalize_condition_path, op?, #to_enum
+ + + + + + + + + + +This class inherits a constructor from WCC::Contentful::Store::Query
+ +
+ + + +164 +165 +166 +167 +168 +169 +170+ |
+
+ # File 'lib/wcc/contentful/store/postgres_store.rb', line 164 + +def count + return @count if @count + + statement, params = finalize_statement('SELECT count(*)') + result = store.exec_query(statement, params) + @count = result.getvalue(0, 0).to_i +end+ |
+
+ + + +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187+ |
+
+ # File 'lib/wcc/contentful/store/postgres_store.rb', line 172 + +def first + return @first if @first + + statement, params = finalize_statement('SELECT t.*', ' LIMIT 1', depth: @options[:include]) + result = store.exec_query(statement, params) + return if result.num_tuples == 0 + + row = result.first + entry = JSON.parse(row['data']) + + if @options[:include] && @options[:include] > 0 + includes = decode_includes(row['includes']) + entry = resolve_includes([entry, includes], @options[:include]) + end + entry +end+ |
+
+ + + +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204+ |
+
+ # File 'lib/wcc/contentful/store/postgres_store.rb', line 189 + +def result_set + return @result_set if @result_set + + statement, params = finalize_statement('SELECT t.*', depth: @options[:include]) + @result_set = + store.exec_query(statement, params) + .lazy.map do |row| + entry = JSON.parse(row['data']) + includes = + (decode_includes(row['includes']) if @options[:include] && @options[:include] > 0) + + [entry, includes] + end +rescue PG::ConnectionBad + [] +end+ |
+
The default query object returned by Stores that extend WCC::Contentful::Store::Base. It exposes several chainable query methods to apply query filters. Enumerating the query executes it, caching the result.
+ + ++ + + Modules: Interface + + + + Classes: Condition + + +
+ + +[ + false, 0, + '0', :'0', + 'f', :f, + 'F', :F, + 'false', :false, # rubocop:disable Lint/BooleanSymbol + 'FALSE', :FALSE, + 'off', :off, + 'OFF', :OFF +].to_set.freeze
%w[fields sys].freeze
Turns a hash into a flat array of individual conditions, where each element can be passed as params to apply_operator.
+Takes a path array in non-normal form and inserts 'sys', 'fields', and the current locale as appropriate to normalize it.
+Called with a filter object by Base#find_by in order to apply the filter.
+Returns a new chained Query that has a new condition.
+A new instance of Query.
+Override this to provide a result set from the Query object itself rather than from calling #execute in the store.
+Executes the query against the store and memoizes the resulting enumerable.
+Returns a new instance of Query.
+ + +
+ + + +29 +30 +31 +32 +33 +34 +35+ |
+
+ # File 'lib/wcc/contentful/store/query.rb', line 29 + +def initialize(store, content_type:, conditions: nil, options: nil, **extra) + @store = store + @content_type = content_type + @conditions = conditions || [] + @options = || {} + @extra = extra +end+ |
+
+ + + +27 +28 +29+ |
+
+ # File 'lib/wcc/contentful/store/query.rb', line 27 + +def conditions + @conditions +end+ |
+
+ + + +27 +28 +29+ |
+
+ # File 'lib/wcc/contentful/store/query.rb', line 27 + +def content_type + @content_type +end+ |
+
+ + + +27 +28 +29+ |
+
+ # File 'lib/wcc/contentful/store/query.rb', line 27 + +def store + @store +end+ |
+
Turns a hash into a flat array of individual conditions, where each element can be passed as params to apply_operator
+ + +
+ + + +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177+ |
+
+ # File 'lib/wcc/contentful/store/query.rb', line 161 + +def flatten_filter_hash(hash, path = []) + hash.flat_map do |(k, v)| + k = k.to_s + if k.include?('.') + k, *rest = k.split('.') + v = { rest.join('.') => v } + end + + if v.is_a? Hash + flatten_filter_hash(v, path + [k]) + elsif op?(k) + { path: path, op: k.to_sym, expected: v } + else + { path: path + [k], op: nil, expected: v } + end + end +end+ |
+
+ + + +179 +180 +181+ |
+
+ # File 'lib/wcc/contentful/store/query.rb', line 179 + +def known_locales + @known_locales = WCC::Contentful.locales.keys +end+ |
+
Takes a path array in non-normal form and inserts 'sys', 'fields', and the current locale as appropriate to normalize it. rubocop:disable Metrics/BlockNesting
+ + +
+ + + +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234+ |
+
+ # File 'lib/wcc/contentful/store/query.rb', line 187 + +def normalize_condition_path(path, context = nil) + context_locale = context[:locale] if context.present? + context_locale ||= 'en-US' + + rev_path = path.reverse + new_path = [] + + current_tuple = [] + current_locale_was_inferred = false + until rev_path.empty? && current_tuple.empty? + raise ArgumentError, "Query too complex: #{path.join('.')}" if new_path.length > 7 + + case current_tuple.length + when 0 + # expect a locale + current_tuple << + if known_locales.include?(rev_path[0]) + current_locale_was_inferred = false + rev_path.shift + else + # infer locale + current_locale_was_inferred = true + context_locale + end + when 1 + # expect a path + current_tuple << rev_path.shift + when 2 + # expect 'sys' or 'fields' + current_tuple << + if RESERVED_NAMES.include?(rev_path[0]) + rev_path.shift + else + # infer 'sys' or 'fields' + current_tuple.last == 'id' ? 'sys' : 'fields' + end + + if current_tuple.last == 'sys' && current_locale_was_inferred + # remove the inferred current locale + current_tuple.shift + end + new_path << current_tuple + current_tuple = [] + end + end + + new_path.flat_map { |x| x }.reverse.freeze +end+ |
+
Called with a filter object by Base#find_by in order to apply the filter. The filter in this case is a hash where the keys are paths and the values are expectations.
+ + +
+ + + +96 +97 +98 +99 +100+ |
+
+ # File 'lib/wcc/contentful/store/query.rb', line 96 + +def apply(filter, context = nil) + self.class.flatten_filter_hash(filter).reduce(self) do |query, cond| + query.apply_operator(cond[:op], cond[:path], cond[:expected], context) + end +end+ |
+
Returns a new chained Query that has a new condition. The new condition represents the WHERE comparison being applied here. The underlying store implementation translates this condition statement into an appropriate query against the datastore.
+ + +
+ + + +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83+ |
+
+ # File 'lib/wcc/contentful/store/query.rb', line 63 + +def apply_operator(operator, field, expected, context = nil) + operator ||= expected.is_a?(Array) ? :in : :eq + raise ArgumentError, "Operator #{operator} not supported" unless respond_to?(operator) + raise ArgumentError, 'value cannot be nil (try using exists: false)' if expected.nil? + + case operator + when :in, :nin, :all + expected = Array(expected) + when :exists + expected = !FALSE_VALUES.include?(expected) + end + + field = field.to_s if field.is_a? Symbol + path = field.is_a?(Array) ? field : field.split('.') + + path = self.class.normalize_condition_path(path, context) + + _append_condition( + Condition.new(path, operator, expected) + ) +end+ |
+
Override this to provide a result set from the Query object itself rather than from calling #execute in the store.
+ + +
+ + + +104 +105 +106+ |
+
+ # File 'lib/wcc/contentful/store/query.rb', line 104 + +def result_set + @result_set ||= store.execute(self) +end+ |
+
Executes the query against the store and memoizes the resulting enumerable.
+ +Subclasses can override this to provide a more efficient implementation.
+
+
+
+
+ + + +22 +23 +24 +25+ |
+
+ # File 'lib/wcc/contentful/store/query.rb', line 22 + +def to_enum + @to_enum ||= + result_set.lazy.map { |row| resolve_includes(row, @options[:include]) } +end+ |
+
rubocop:disable Lint/ConstantDefinitionInBlock
+ + +%w[id type linkType].freeze
+ + + +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263+ |
+
+ # File 'lib/wcc/contentful/store/query.rb', line 242 + +def path_tuples + @path_tuples ||= + [].tap do |arr| + remaining = path.dup + until remaining.empty? + locale = nil + link_sys = nil + link_field = nil + + sys_or_fields = remaining.shift + field = remaining.shift + locale = remaining.shift if sys_or_fields == 'fields' + + if remaining[0] == 'sys' && LINK_KEYS.include?(remaining[1]) + link_sys = remaining.shift + link_field = remaining.shift + end + + arr << [sys_or_fields, field, locale, link_sys, link_field].compact + end + end +end+ |
+
This module represents the common interface of queries that must be returned by a store's #find_all implementation. It is documentation ONLY and does not add functionality.
+ +This is distinct from WCC::Contentful::Store::Query, because certain helpers exposed publicly by that abstract class are not part of the actual interface and can change without a major version update.
+ + +The set of operators that can be applied to a query. Not all stores implement all operators. At a bare minimum a store must implement #eq.
+ + +%i[ + eq + ne + all + in + nin + exists + lt + lte + gt + gte + query + match +].freeze
Called with a filter object in order to apply the filter.
+Applies an equality condition to the query.
+Called with a filter object in order to apply the filter. The filter in this case is a hash where the keys are paths and the values are expectations.
+ +sig +
field: T.any(T::String),
+ expected: T.untyped,
+ context: T.nilable(T::Hash[T.untyped, T.untyped])
+).returns(T.self_type)
+
+
+
+
+ + + +59 +60 +61+ |
+
+ # File 'lib/wcc/contentful/store/query/interface.rb', line 59 + +def apply(_filter, _context = nil) + raise NotImplementedError, "#{self.class} does not implement #apply" +end+ |
+
Applies an equality condition to the query. The underlying store translates this into a '==' check.
+ +sig +
field: T.any(T::String),
+ expected: T.untyped,
+ context: T.nilable(T::Hash[T.untyped, T.untyped])
+).returns(T.self_type)
+
+
+
+
+ + + +46 +47 +48+ |
+
+ # File 'lib/wcc/contentful/store/query/interface.rb', line 46 + +def eq(_field, _expected, _context = nil) + raise NotImplementedError, "#{self.class} does not implement #eq" +end+ |
+
The SyncEngine is used to keep the currently configured store up to date using the Sync API. It is available on the WCC::Contentful::Services instance, and the application is responsible to periodically call #next in order to hit the sync API and update the store.
+ +If you have mounted the WCC::Contentful::Engine, AND the configured store is one that can be synced (i.e. it responds to `:index`), then the WCC::Contentful::WebhookController will call #next automatically anytime a webhook is received. Otherwise you should hook up to the Webhook events and call the sync engine via your initializer:
+ +WCC::Contentful::Events.subscribe(proc do |event|
+ WCC::Contentful::Services.instance.sync_engine.next(up_to: event.dig('sys', 'id'))
+end, with: :call)
+
+
+
+ + + + + + Classes: Job + + +
+ + + + +Returns the value of attribute client.
+Returns the value of attribute options.
+Returns the value of attribute store.
+A new instance of SyncEngine.
+Gets the next increment of data from the Sync API.
+Returns a new instance of SyncEngine.
+ + +
+ + + +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62+ |
+
+ # File 'lib/wcc/contentful/sync_engine.rb', line 40 + +def initialize(client: nil, store: nil, state: nil, **) + @options = { + key: "sync:#{object_id}" + }.merge!().freeze + + @state_key = @options[:key] || "sync:#{object_id}" + @client = client || WCC::Contentful::Services.instance.client + @mutex = Mutex.new + + if store + unless %i[index index? find].all? { |m| store.respond_to?(m) } + raise ArgumentError, ':store param must implement the Store interface' + end + + @store = store + end + if state + @state = token_wrapper_factory(state) + raise ArgumentError, ':state param must be a String or Hash' unless @state.is_a? Hash + raise ArgumentError, ':state param must be of sys.type = "token"' unless @state.dig('sys', 'type') == 'token' + end + raise ArgumentError, 'either :state or :store must be provided' unless @state || @store +end+ |
+
Returns the value of attribute client.
+ + +
+ + + +34 +35 +36+ |
+
+ # File 'lib/wcc/contentful/sync_engine.rb', line 34 + +def client + @client +end+ |
+
Returns the value of attribute options.
+ + +
+ + + +34 +35 +36+ |
+
+ # File 'lib/wcc/contentful/sync_engine.rb', line 34 + +def + @options +end+ |
+
Returns the value of attribute store.
+ + +
+ + + +34 +35 +36+ |
+
+ # File 'lib/wcc/contentful/sync_engine.rb', line 34 + +def store + @store +end+ |
+
+ + + +104 +105 +106 +107 +108 +109+ |
+
+ # File 'lib/wcc/contentful/sync_engine.rb', line 104 + +def emit_event(event) + type = event.dig('sys', 'type') + raise ArgumentError, "Unknown event type #{event}" unless type.present? + + broadcast(type, event) +end+ |
+
+ + + +111 +112 +113 +114+ |
+
+ # File 'lib/wcc/contentful/sync_engine.rb', line 111 + +def emit_sync_complete(events) + event = WCC::Contentful::Event::SyncComplete.new(events, source: self) + broadcast('SyncComplete', event) +end+ |
+
Gets the next increment of data from the Sync API. If the configured store responds to `:index`, that will be called with each item in the Sync response to update the store. If a block is passed, that block will be evaluated with each item in the response.
+ + +
+ + + +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102+ |
+
+ # File 'lib/wcc/contentful/sync_engine.rb', line 73 + +def next(up_to_id: nil) + id_found = up_to_id.nil? + all_events = [] + + @mutex.synchronize do + @state ||= read_state || token_wrapper_factory(nil) + sync_token = @state['token'] + + next_sync_token = + client.sync(sync_token: sync_token) do |item| + id = item.dig('sys', 'id') + id_found ||= id == up_to_id + + store.index(item) if store&.index? + event = WCC::Contentful::Event.from_raw(item, source: self) + yield(event) if block_given? + emit_event(event) + + # Only keep the "sys" not the content in case we have a large space + all_events << WCC::Contentful::Event.from_raw(item.slice('sys'), source: self) + end + + @state = @state.merge('token' => next_sync_token) + write_state + end + + emit_sync_complete(all_events) + + [id_found, all_events.length] +end+ |
+
+ + + +36 +37 +38+ |
+
+ # File 'lib/wcc/contentful/sync_engine.rb', line 36 + +def should_sync? + store&.index? +end+ |
+
+ + + +30 +31 +32+ |
+
+ # File 'lib/wcc/contentful/sync_engine.rb', line 30 + +def state + (@state&.dup || token_wrapper_factory(nil)).freeze +end+ |
+
This job uses the Contentful Sync API to update the configured store with the latest data from Contentful.
+ + +Calls the Contentful Sync API and updates the configured store with the returned data.
+Enqueues an ActiveJob job to invoke WCC::Contentful.sync! after a given amount of time.
+
+ + + +144 +145 +146+ |
+
+ # File 'lib/wcc/contentful/sync_engine.rb', line 144 + +def configuration + @configuration ||= WCC::Contentful.configuration +end+ |
+
+ + + +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162+ |
+
+ # File 'lib/wcc/contentful/sync_engine.rb', line 152 + +def perform(event = nil) + return unless services.sync_engine&.should_sync? + + up_to_id = nil + retry_count = 0 + if event + up_to_id = event[:up_to_id] || event.dig('sys', 'id') + retry_count = event[:retry_count] if event[:retry_count] + end + sync!(up_to_id: up_to_id, retry_count: retry_count) +end+ |
+
+ + + +148 +149 +150+ |
+
+ # File 'lib/wcc/contentful/sync_engine.rb', line 148 + +def services + @services ||= WCC::Contentful::Services.instance +end+ |
+
Calls the Contentful Sync API and updates the configured store with the returned data.
+ + +
+ + + +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193+ |
+
+ # File 'lib/wcc/contentful/sync_engine.rb', line 172 + +def sync!(up_to_id: nil, retry_count: 0) + id_found, count = services.sync_engine.next(up_to_id: up_to_id) + + next_sync_token = services.sync_engine.state['token'] + + logger.info "Synced #{count} entries. Next sync token:\n #{next_sync_token}" + unless id_found + if retry_count >= configuration.sync_retry_limit + logger.error "Unable to find item with id '#{up_to_id}' on the Sync API. " \ + "Abandoning after #{retry_count} retries." + else + wait = (2**retry_count) * configuration.sync_retry_wait.seconds + logger.info "Unable to find item with id '#{up_to_id}' on the Sync API. " \ + "Retrying after #{wait.inspect} " \ + "(#{configuration.sync_retry_limit - retry_count} retries remaining)" + + self.class.set(wait: wait) + .perform_later(up_to_id: up_to_id, retry_count: retry_count + 1) + end + end + next_sync_token +end+ |
+
Enqueues an ActiveJob job to invoke WCC::Contentful.sync! after a given amount of time.
+ + +
+ + + +197 +198 +199 +200+ |
+
+ # File 'lib/wcc/contentful/sync_engine.rb', line 197 + +def sync_later!(up_to_id: nil, wait: 10.seconds) + self.class.set(wait: wait) + .perform_later(up_to_id: up_to_id) +end+ |
+
+ + + Modules: Attributes, Double, Factory + + + + +
+ + + + + + + + + +{ + String: 'test', + Int: 0, + Float: 0.0, + DateTime: Time.at(0).to_s, + Boolean: false, + Json: ->(_f) { {} }, + Coordinates: ->(_f) { {} }, + Asset: ->(f) { + WCC::Contentful::Link.new( + "fake-#{f.name}-#{SecureRandom.urlsafe_base64[1..6]}", + :Asset + ).raw + }, + Link: ->(f) { + WCC::Contentful::Link.new( + "fake-#{f.name}-#{SecureRandom.urlsafe_base64[1..6]}", + :Link + ).raw + } +}.freeze
Gets the default value for a contentful IndexedRepresentation::Field.
+Get a hash of default values for all attributes unique to the given Contentful model.
+
+ + + +27 +28 +29+ |
+
+ # File 'lib/wcc/contentful/test/attributes.rb', line 27 + +def [](key) + DEFAULTS[key] +end+ |
+
Gets the default value for a contentful IndexedRepresentation::Field. This comes from the 'content_type_definition' of a contentful model class.
+ + +
+ + + +42 +43 +44 +45 +46 +47 +48 +49 +50+ |
+
+ # File 'lib/wcc/contentful/test/attributes.rb', line 42 + +def default_value(field) + return [] if field.array + return unless field.required + + val = DEFAULTS[field.type] + return val.call(field) if val.respond_to?(:call) + + val +end+ |
+
Get a hash of default values for all attributes unique to the given Contentful model.
+ + +
+ + + +33 +34 +35 +36 +37+ |
+
+ # File 'lib/wcc/contentful/test/attributes.rb', line 33 + +def defaults(const) + const.content_type_definition.fields.each_with_object({}) do |(name, f), h| + h[name.to_sym] = h[name.underscore.to_sym] = default_value(f) + end +end+ |
+
Builds a rspec double of the Contentful model for the given content_type.
+Builds an rspec double of a Contentful image asset, including the file URL and details.
+Builds a rspec double of the Contentful model for the given content_type. All attributes that are known to be required fields on the content type will return a default value based on the field type.
+ + +
+ + + +10 +11 +12 +13 +14 +15 +16 +17 +18+ |
+
+ # File 'lib/wcc/contentful/test/double.rb', line 10 + +def contentful_double(const, **attrs) + const = WCC::Contentful::Model.resolve_constant(const.to_s) unless const.respond_to?(:content_type_definition) + attrs.symbolize_keys! + + bad_attrs = attrs.reject { |a| const.instance_methods.include?(a) } + raise ArgumentError, "Attribute(s) do not exist on #{const}: #{bad_attrs.keys}" if bad_attrs.any? + + double(attrs[:name] || attrs[:id] || nil, defaults(const).merge(attrs)) +end+ |
+
Builds an rspec double of a Contentful image asset, including the file URL and details. These fields can be overridden.
+ + +
+ + + +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67+ |
+
+ # File 'lib/wcc/contentful/test/double.rb', line 23 + +def contentful_image_double(**attrs) + attrs = { + title: WCC::Contentful::Test::Attributes[:String], + description: WCC::Contentful::Test::Attributes[:String], + file: { + url: '//images.ctfassets.net/7yx6/2rak/test.jpg', + details: { + image: { + width: 0, + height: 0 + } + } + } + }.deep_merge!(attrs) + + attrs[:file] = OpenStruct.new(attrs[:file]) if attrs[:file] + + attrs[:raw] = { + sys: { + space: { + sys: { + type: 'Link', + linkType: 'Space', + id: ENV.fetch('CONTENTFUL_SPACE_ID', nil) + } + }, + id: SecureRandom.urlsafe_base64, + type: 'Asset', + createdAt: Time.now.to_s(:iso8601), + updatedAt: Time.now.to_s(:iso8601), + environment: { + sys: { + id: 'master', + type: 'Link', + linkType: 'Environment' + } + }, + revision: rand(100), + locale: 'en-US' + }, + fields: attrs.transform_values { |v| { 'en-US' => v } } + } + + double(attrs) +end+ |
+
Builds a in-memory instance of the Contentful model for the given content_type.
+Builds a in-memory instance of the Contentful model for the given content_type. All attributes that are known to be required fields on the content type will return a default value based on the field type.
+ + +
+ + + +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42+ |
+
+ # File 'lib/wcc/contentful/test/factory.rb', line 10 + +def contentful_create(const, context = nil, **attrs) + const = WCC::Contentful::Model.resolve_constant(const.to_s) unless const.respond_to?(:content_type_definition) + attrs = attrs.transform_keys { |a| a.to_s.camelize(:lower) } + + id = attrs.delete('id') + sys = attrs.delete('sys') + raw = attrs.delete('raw') || default_raw(const, id) + bad_attrs = attrs.reject { |a| const.content_type_definition.fields.key?(a) } + raise ArgumentError, "Attribute(s) do not exist on #{const}: #{bad_attrs.keys}" if bad_attrs.any? + + raw['sys'].merge!(sys) if sys + + attrs.each do |k, v| + field = const.content_type_definition.fields[k] + + raw_value = v + raw_value = to_raw(v, field.type) if %i[Asset Link].include?(field.type) + raw['fields'][field.name][raw.dig('sys', 'locale')] = raw_value + end + + instance = const.new(raw, context) + + attrs.each do |k, v| + field = const.content_type_definition.fields[k] + next unless %i[Asset Link].include?(field.type) + + unless field.array ? v.any? { |i| i.is_a?(String) } : v.is_a?(String) + instance.instance_variable_set("@#{field.name}_resolved", v) + end + end + + instance +end+ |
+
The WebhookController is mounted by the WCC::Contentful::Engine to receive webhook events from Contentful. It passes these webhook events to the jobs configured in WCC::Contentful::Configuration#webhook_jobs
+ + +
+ + + +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32+ |
+
+ # File 'app/controllers/wcc/contentful/webhook_controller.rb', line 20 + +def receive + params.require('sys').require(%w[id type]) + params.permit('sys', 'fields') + event = params.slice('sys', 'fields').permit!.to_h + + return unless check_environment(event) + + # Immediately update the store, we may update again later using SyncEngine::Job. + store.index(event) if store.index? + + event = WCC::Contentful::Event.from_raw(event, source: self) + emit_event(event) +end+ |
+
+ + + +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43+ |
+
+ # File 'app/jobs/wcc/contentful/webhook_enable_job.rb', line 19 + +def enable_webhook(client, receive_url:, webhook_username: nil, webhook_password: nil) + webhook = client.webhook_definitions.items.find { |w| w['url'] == receive_url } + logger.debug "existing webhook: #{webhook.inspect}" if webhook + return if webhook + + body = { + 'name' => 'WCC::Contentful webhook', + 'url' => receive_url, + 'topics' => [ + '*.publish', + '*.unpublish' + ], + 'filters' => webhook_filters + } + body['httpBasicUsername'] = webhook_username if webhook_username.present? + body['httpBasicPassword'] = webhook_password if webhook_password.present? + + begin + resp = client.post_webhook_definition(body) + logger.info "Created webhook: #{resp.raw.dig('sys', 'id')}" + rescue WCC::Contentful::SimpleClient::ApiError => e + logger.error "#{e.response.code}: #{e.response.raw}" if e.response + raise + end +end+ |
+
+ + + +10 +11 +12 +13 +14 +15 +16 +17+ |
+
+ # File 'app/jobs/wcc/contentful/webhook_enable_job.rb', line 10 + +def perform(args = {}) + args = default_configuration.merge!(args) + + client = WCC::Contentful::SimpleClient::Management.new( + **args + ) + enable_webhook(client, args.slice(:receive_url, :webhook_username, :webhook_password)) +end+ |
+
+
+
+
|
+
+
+
|
+
+
+
|
+
t |