wcc-contentful 0.0.3 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.circleci/config.yml +51 -0
- data/.gitignore +8 -0
- data/.rspec +1 -0
- data/.rubocop.yml +240 -0
- data/.rubocop_todo.yml +13 -0
- data/CHANGELOG.md +7 -1
- data/Gemfile +4 -2
- data/Guardfile +36 -0
- data/README.md +1 -1
- data/Rakefile +5 -3
- data/bin/rspec +3 -0
- data/lib/generators/wcc/USAGE +24 -0
- data/lib/generators/wcc/menu_generator.rb +67 -0
- data/lib/generators/wcc/templates/.keep +0 -0
- data/lib/generators/wcc/templates/Procfile +3 -0
- data/lib/generators/wcc/templates/contentful_shell_wrapper +342 -0
- data/lib/generators/wcc/templates/menu/generated_add_menus.ts +85 -0
- data/lib/generators/wcc/templates/menu/menu.rb +25 -0
- data/lib/generators/wcc/templates/menu/menu_button.rb +25 -0
- data/lib/generators/wcc/templates/release +9 -0
- data/lib/generators/wcc/templates/wcc_contentful.rb +18 -0
- data/lib/wcc/contentful.rb +93 -26
- data/lib/wcc/contentful/client_ext.rb +15 -0
- data/lib/wcc/contentful/configuration.rb +93 -0
- data/lib/wcc/contentful/content_type_indexer.rb +153 -0
- data/lib/wcc/contentful/exceptions.rb +34 -0
- data/lib/wcc/contentful/graphql.rb +15 -0
- data/lib/wcc/contentful/graphql/builder.rb +172 -0
- data/lib/wcc/contentful/graphql/types.rb +54 -0
- data/lib/wcc/contentful/helpers.rb +28 -0
- data/lib/wcc/contentful/indexed_representation.rb +111 -0
- data/lib/wcc/contentful/model.rb +24 -0
- data/lib/wcc/contentful/model/menu.rb +7 -0
- data/lib/wcc/contentful/model/menu_button.rb +15 -0
- data/lib/wcc/contentful/model_builder.rb +151 -0
- data/lib/wcc/contentful/model_validators.rb +64 -0
- data/lib/wcc/contentful/model_validators/dsl.rb +165 -0
- data/lib/wcc/contentful/simple_client.rb +127 -0
- data/lib/wcc/contentful/simple_client/response.rb +160 -0
- data/lib/wcc/contentful/store.rb +8 -0
- data/lib/wcc/contentful/store/cdn_adapter.rb +79 -0
- data/lib/wcc/contentful/store/memory_store.rb +75 -0
- data/lib/wcc/contentful/store/postgres_store.rb +132 -0
- data/lib/wcc/contentful/version.rb +3 -1
- data/wcc-contentful.gemspec +49 -24
- metadata +261 -16
- data/lib/wcc/contentful/redirect.rb +0 -33
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This file reopens the "Menu" class which was dynamically
|
4
|
+
# created by the WCC::Contentful gem. This class does not need to do anything,
|
5
|
+
# the attributes have already been defined based on the `content_type` returned
|
6
|
+
# from the Contentful API. However you can reopen the class to add functionality.
|
7
|
+
class WCC::Contentful::Model::Menu < WCC::Contentful::Model
|
8
|
+
|
9
|
+
# Add custom validations to ensure that app-specific properties exist:
|
10
|
+
# validate_field :foo, :String, :required
|
11
|
+
# validate_field :bar_links, :Array, link_to: %w[bar baz]
|
12
|
+
|
13
|
+
# Override functionality or add utilities
|
14
|
+
#
|
15
|
+
# # Example: override equality
|
16
|
+
# def ===(other)
|
17
|
+
# ...
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# # Example: override "name" attribute to always be camelized.
|
21
|
+
# # `@name` is populated by the gem in the initializer.
|
22
|
+
# def name
|
23
|
+
# @name_camelized ||= @name.camelize(true)
|
24
|
+
# end
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This file reopens the "MenuButton" class which was dynamically
|
4
|
+
# created by the WCC::Contentful gem. This class does not need to do anything,
|
5
|
+
# the attributes have already been defined based on the `content_type` returned
|
6
|
+
# from the Contentful API. However you can reopen the class to add functionality.
|
7
|
+
class WCC::Contentful::Model::MenuButton < WCC::Contentful::Model
|
8
|
+
|
9
|
+
# Add custom validations to ensure that app-specific properties exist:
|
10
|
+
# validate_field :foo, :String, :required
|
11
|
+
# validate_field :bar_links, :Array, link_to: %w[bar baz]
|
12
|
+
|
13
|
+
# Override functionality or add utilities
|
14
|
+
#
|
15
|
+
# # Example: override equality
|
16
|
+
# def ===(other)
|
17
|
+
# ...
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# # Example: override "text" attribute to always be camelized.
|
21
|
+
# # `@text` is populated by the gem in the initializer.
|
22
|
+
# def text
|
23
|
+
# @text_camelized ||= @text.camelize(true)
|
24
|
+
# end
|
25
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
|
2
|
+
WCC::Contentful.configure do |config|
|
3
|
+
# Required
|
4
|
+
config.access_token = # Contentful CDN access token
|
5
|
+
config.space = # Contentful Space ID
|
6
|
+
|
7
|
+
# Optional
|
8
|
+
config.management_token = # Contentful API management token
|
9
|
+
config.default_locale = # Set default locale, if left blank this is 'en-US'
|
10
|
+
config.content_delivery = # :direct, :eager_sync, or :lazy_sync
|
11
|
+
config.sync_store = # :memory, :postgres, or a custom implementation
|
12
|
+
end
|
13
|
+
|
14
|
+
# Download content types, build models, and sync content
|
15
|
+
WCC::Contentful.init!
|
16
|
+
|
17
|
+
# Validate that models conform to a defined specification
|
18
|
+
WCC::Contentful.validate_models! unless defined?(Rails) && Rails.env.development?
|
data/lib/wcc/contentful.rb
CHANGED
@@ -1,38 +1,105 @@
|
|
1
|
-
|
2
|
-
require 'contentful_model'
|
1
|
+
# frozen_string_literal: true
|
3
2
|
|
4
|
-
|
5
|
-
module Contentful
|
3
|
+
require 'wcc/contentful/version'
|
6
4
|
|
7
|
-
|
8
|
-
|
9
|
-
end
|
5
|
+
require 'active_support'
|
6
|
+
require 'active_support/core_ext/object'
|
10
7
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
8
|
+
require 'wcc/contentful/configuration'
|
9
|
+
require 'wcc/contentful/exceptions'
|
10
|
+
require 'wcc/contentful/helpers'
|
11
|
+
require 'wcc/contentful/simple_client'
|
12
|
+
require 'wcc/contentful/store'
|
13
|
+
require 'wcc/contentful/content_type_indexer'
|
14
|
+
require 'wcc/contentful/model_validators'
|
15
|
+
require 'wcc/contentful/model'
|
16
|
+
require 'wcc/contentful/model_builder'
|
17
|
+
|
18
|
+
module WCC::Contentful
|
19
|
+
class << self
|
20
|
+
attr_reader :configuration
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.client
|
24
|
+
configuration&.client
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.configure
|
28
|
+
@configuration ||= Configuration.new
|
29
|
+
yield(configuration)
|
16
30
|
|
17
|
-
|
18
|
-
attr_accessor :access_token, :space, :default_locale
|
31
|
+
configuration.configure_contentful
|
19
32
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
33
|
+
raise ArgumentError, 'Please provide "space" ID' unless configuration.space.present?
|
34
|
+
raise ArgumentError, 'Please provide "access_token"' unless configuration.access_token.present?
|
35
|
+
|
36
|
+
configuration
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.init!
|
40
|
+
raise ArgumentError, 'Please first call WCC:Contentful.configure' if configuration.nil?
|
41
|
+
|
42
|
+
# we want as much as possible the raw JSON from the API
|
43
|
+
content_types_resp =
|
44
|
+
if configuration.management_client
|
45
|
+
configuration.management_client.content_types(limit: 1000)
|
46
|
+
else
|
47
|
+
configuration.client.content_types(limit: 1000)
|
24
48
|
end
|
49
|
+
@content_types = content_types_resp.items
|
25
50
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
config.space = WCC::Contentful.configuration.space
|
30
|
-
config.default_locale = WCC::Contentful.configuration.default_locale
|
31
|
-
end
|
51
|
+
indexer =
|
52
|
+
ContentTypeIndexer.new.tap do |ixr|
|
53
|
+
@content_types.each { |type| ixr.index(type) }
|
32
54
|
end
|
55
|
+
@types = indexer.types
|
56
|
+
|
57
|
+
case configuration.content_delivery
|
58
|
+
when :eager_sync
|
59
|
+
store = configuration.sync_store
|
60
|
+
|
61
|
+
client.sync(initial: true).items.each do |item|
|
62
|
+
# TODO: enrich existing type data using Sync::Indexer
|
63
|
+
store.index(item.dig('sys', 'id'), item)
|
64
|
+
end
|
65
|
+
WCC::Contentful::Model.store = store
|
66
|
+
when :direct
|
67
|
+
store = Store::CDNAdapter.new(client)
|
68
|
+
WCC::Contentful::Model.store = store
|
69
|
+
end
|
70
|
+
|
71
|
+
WCC::Contentful::ModelBuilder.new(@types).build_models
|
72
|
+
|
73
|
+
# Extend all model types w/ validation & extra fields
|
74
|
+
@types.each_value do |t|
|
75
|
+
file = File.dirname(__FILE__) + "/contentful/model/#{t.name.underscore}.rb"
|
76
|
+
require file if File.exist?(file)
|
33
77
|
end
|
34
78
|
|
79
|
+
return unless defined?(Rails)
|
80
|
+
Dir[Rails.root.join('lib/wcc/contentful/model/**/*.rb')].each { |file| require file }
|
35
81
|
end
|
36
|
-
end
|
37
82
|
|
38
|
-
|
83
|
+
def self.validate_models!
|
84
|
+
schema =
|
85
|
+
Dry::Validation.Schema do
|
86
|
+
WCC::Contentful::Model.all_models.each do |klass|
|
87
|
+
next unless klass.schema
|
88
|
+
ct = klass.try(:content_type) || klass.name.demodulize
|
89
|
+
required(ct).schema(klass.schema)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
content_types = WCC::Contentful::ModelValidators.transform_content_types_for_validation(
|
94
|
+
@content_types
|
95
|
+
)
|
96
|
+
errors = schema.call(content_types)
|
97
|
+
raise WCC::Contentful::ValidationError, errors.errors unless errors.success?
|
98
|
+
end
|
99
|
+
|
100
|
+
# TODO: https://zube.io/watermarkchurch/development/c/2234 init graphql
|
101
|
+
# def self.init_graphql!
|
102
|
+
# require 'wcc/contentful/graphql'
|
103
|
+
# etc...
|
104
|
+
# end
|
105
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Contentful::Client
|
4
|
+
class << self
|
5
|
+
alias_method :old_get_http, :get_http
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.get_http(url, query, headers = {}, proxy = {})
|
9
|
+
if override = WCC::Contentful.configuration.override_get_http
|
10
|
+
override.call(url, query, headers, proxy)
|
11
|
+
else
|
12
|
+
old_get_http(url, query, headers, proxy)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'http'
|
4
|
+
|
5
|
+
class WCC::Contentful::Configuration
|
6
|
+
ATTRIBUTES = %i[
|
7
|
+
access_token
|
8
|
+
management_token
|
9
|
+
space
|
10
|
+
default_locale
|
11
|
+
content_delivery
|
12
|
+
override_get_http
|
13
|
+
].freeze
|
14
|
+
attr_accessor(*ATTRIBUTES)
|
15
|
+
|
16
|
+
CDN_METHODS = [
|
17
|
+
:eager_sync,
|
18
|
+
# TODO: :lazy_sync
|
19
|
+
:direct
|
20
|
+
].freeze
|
21
|
+
|
22
|
+
SYNC_STORES = {
|
23
|
+
memory: ->(_config) { WCC::Contentful::Store::MemoryStore.new },
|
24
|
+
postgres: ->(_config) {
|
25
|
+
require_relative 'store/postgres_store'
|
26
|
+
WCC::Contentful::Store::PostgresStore.new(ENV['POSTGRES_CONNECTION'])
|
27
|
+
}
|
28
|
+
}.freeze
|
29
|
+
|
30
|
+
def content_delivery=(symbol)
|
31
|
+
raise ArgumentError, "Please set one of #{CDN_METHODS}" unless CDN_METHODS.include?(symbol)
|
32
|
+
@content_delivery = symbol
|
33
|
+
end
|
34
|
+
|
35
|
+
def sync_store=(symbol)
|
36
|
+
if symbol.is_a? Symbol
|
37
|
+
unless SYNC_STORES.keys.include?(symbol)
|
38
|
+
raise ArgumentError, "Please use one of #{SYNC_STORES.keys}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
@sync_store = symbol
|
42
|
+
end
|
43
|
+
|
44
|
+
def sync_store
|
45
|
+
@sync_store = SYNC_STORES[@sync_store].call(self) if @sync_store.is_a? Symbol
|
46
|
+
@sync_store ||= Store::MemoryStore.new
|
47
|
+
end
|
48
|
+
|
49
|
+
# A proc which overrides the "get_http" function in Contentful::Client.
|
50
|
+
# All interaction with Contentful will go through this function.
|
51
|
+
# Should be a lambda like: ->(url, query, headers = {}, proxy = {}) { ... }
|
52
|
+
attr_writer :override_get_http
|
53
|
+
|
54
|
+
def initialize
|
55
|
+
@access_token = ''
|
56
|
+
@management_token = ''
|
57
|
+
@space = ''
|
58
|
+
@default_locale = nil
|
59
|
+
@content_delivery = :direct
|
60
|
+
@sync_store = :memory
|
61
|
+
end
|
62
|
+
|
63
|
+
attr_reader :client
|
64
|
+
attr_reader :management_client
|
65
|
+
|
66
|
+
def configure_contentful
|
67
|
+
@client = nil
|
68
|
+
@management_client = nil
|
69
|
+
|
70
|
+
if defined?(::ContentfulModel)
|
71
|
+
ContentfulModel.configure do |config|
|
72
|
+
config.access_token = access_token
|
73
|
+
config.management_token = management_token if management_token.present?
|
74
|
+
config.space = space
|
75
|
+
config.default_locale = default_locale || 'en-US'
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
require_relative 'client_ext' if defined?(::Contentful)
|
80
|
+
|
81
|
+
@client = WCC::Contentful::SimpleClient::Cdn.new(
|
82
|
+
access_token: access_token,
|
83
|
+
space: space,
|
84
|
+
default_locale: default_locale
|
85
|
+
)
|
86
|
+
return unless management_token.present?
|
87
|
+
@management_client = WCC::Contentful::SimpleClient::Management.new(
|
88
|
+
management_token: management_token,
|
89
|
+
space: space,
|
90
|
+
default_locale: default_locale
|
91
|
+
)
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'indexed_representation'
|
4
|
+
|
5
|
+
module WCC::Contentful
|
6
|
+
class ContentTypeIndexer
|
7
|
+
include WCC::Contentful::Helpers
|
8
|
+
|
9
|
+
attr_reader :types
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@types = IndexedRepresentation.new({
|
13
|
+
'Asset' => create_asset_type
|
14
|
+
})
|
15
|
+
end
|
16
|
+
|
17
|
+
def index(content_type)
|
18
|
+
content_type =
|
19
|
+
if content_type.respond_to?(:fields)
|
20
|
+
create_type(content_type.id, content_type.fields)
|
21
|
+
else
|
22
|
+
create_type(content_type.dig('sys', 'id'), content_type['fields'])
|
23
|
+
end
|
24
|
+
|
25
|
+
@types[content_type.content_type] = content_type
|
26
|
+
end
|
27
|
+
|
28
|
+
def create_type(content_type_id, fields)
|
29
|
+
content_type = IndexedRepresentation::ContentType.new({
|
30
|
+
name: constant_from_content_type(content_type_id),
|
31
|
+
content_type: content_type_id
|
32
|
+
})
|
33
|
+
|
34
|
+
fields.each do |f|
|
35
|
+
field = create_field(f)
|
36
|
+
content_type.fields[field.name] = field
|
37
|
+
end
|
38
|
+
|
39
|
+
content_type
|
40
|
+
end
|
41
|
+
|
42
|
+
# hardcoded because the Asset type is a "magic type" in their system
|
43
|
+
def create_asset_type
|
44
|
+
IndexedRepresentation::ContentType.new({
|
45
|
+
name: 'Asset',
|
46
|
+
content_type: 'Asset',
|
47
|
+
fields: {
|
48
|
+
'title' => { name: 'title', type: :String },
|
49
|
+
'description' => { name: 'description', type: :String },
|
50
|
+
'file' => { name: 'file', type: :Json }
|
51
|
+
}
|
52
|
+
})
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def create_field(field)
|
58
|
+
if field.respond_to?(:raw)
|
59
|
+
create_field_from_raw(field.raw)
|
60
|
+
elsif field.respond_to?(:to_h)
|
61
|
+
create_field_from_raw(field.to_h)
|
62
|
+
else
|
63
|
+
create_field_from_managed(field)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def create_field_from_managed(managed_field)
|
68
|
+
field = IndexedRepresentation::Field.new({
|
69
|
+
name: managed_field.id,
|
70
|
+
type: find_field_type(managed_field),
|
71
|
+
required: managed_field.required
|
72
|
+
})
|
73
|
+
field.array = true if managed_field.type == 'Array'
|
74
|
+
|
75
|
+
if field.type == :Link
|
76
|
+
validations =
|
77
|
+
if field.array
|
78
|
+
managed_field.items.validations
|
79
|
+
else
|
80
|
+
managed_field.validations
|
81
|
+
end
|
82
|
+
field.link_types = resolve_managed_link_types(validations) if validations.present?
|
83
|
+
end
|
84
|
+
field
|
85
|
+
end
|
86
|
+
|
87
|
+
def create_field_from_raw(raw_field)
|
88
|
+
field_name = raw_field['id']
|
89
|
+
field = IndexedRepresentation::Field.new({
|
90
|
+
name: field_name,
|
91
|
+
type: find_field_type(raw_field),
|
92
|
+
required: raw_field['required']
|
93
|
+
})
|
94
|
+
field.array = true if raw_field['type'] == 'Array'
|
95
|
+
|
96
|
+
if field.type == :Link
|
97
|
+
validations =
|
98
|
+
if field.array
|
99
|
+
raw_field.dig('items', 'validations')
|
100
|
+
else
|
101
|
+
raw_field['validations']
|
102
|
+
end
|
103
|
+
field.link_types = resolve_raw_link_types(validations) if validations.present?
|
104
|
+
end
|
105
|
+
field
|
106
|
+
end
|
107
|
+
|
108
|
+
def find_field_type(field)
|
109
|
+
# 'Symbol' | 'Text' | 'Integer' | 'Number' | 'Date' | 'Boolean' |
|
110
|
+
# 'Object' | 'Location' | 'Array' | 'Link'
|
111
|
+
case raw_type = field.try(:type) || field['type']
|
112
|
+
when 'Symbol', 'Text'
|
113
|
+
:String
|
114
|
+
when 'Integer'
|
115
|
+
:Int
|
116
|
+
when 'Number'
|
117
|
+
:Float
|
118
|
+
when 'Date'
|
119
|
+
:DateTime
|
120
|
+
when 'Boolean'
|
121
|
+
:Boolean
|
122
|
+
when 'Object'
|
123
|
+
:Json
|
124
|
+
when 'Location'
|
125
|
+
:Coordinates
|
126
|
+
when 'Array'
|
127
|
+
find_field_type(field.try(:items) || field['items'])
|
128
|
+
when 'Link'
|
129
|
+
case link_type = field.try(:link_type) || field['linkType']
|
130
|
+
when 'Entry'
|
131
|
+
:Link
|
132
|
+
when 'Asset'
|
133
|
+
:Asset
|
134
|
+
else
|
135
|
+
raise ArgumentError,
|
136
|
+
"Unknown link type #{link_type} for field #{field.try(:id) || field['id']}"
|
137
|
+
end
|
138
|
+
else
|
139
|
+
raise ArgumentError, "unknown field type #{raw_type} for field #{field.try(:id) || field['id']}"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def resolve_managed_link_types(validations)
|
144
|
+
validation = validations.find { |v| v.link_content_type.present? }
|
145
|
+
validation.link_content_type if validation.present?
|
146
|
+
end
|
147
|
+
|
148
|
+
def resolve_raw_link_types(validations)
|
149
|
+
validation = validations.find { |v| v['linkContentType'].present? }
|
150
|
+
validation['linkContentType'] if validation.present?
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|