wcc-contentful 0.0.3 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|