wcc-contentful 0.3.0.pre.rc3 → 0.3.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 +26 -0
- data/.rspec +1 -0
- data/.rubocop.yml +242 -0
- data/.rubocop_todo.yml +19 -0
- data/.travis.yml +5 -0
- data/CHANGELOG.md +180 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Guardfile +58 -0
- data/LICENSE.txt +21 -0
- data/Rakefile +8 -0
- data/bin/console +3 -4
- data/bin/rails +0 -2
- data/lib/generators/wcc/USAGE +24 -0
- data/lib/generators/wcc/model_generator.rb +90 -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 +385 -0
- data/lib/generators/wcc/templates/menu/generated_add_menus.ts +192 -0
- data/lib/generators/wcc/templates/menu/models/menu.rb +23 -0
- data/lib/generators/wcc/templates/menu/models/menu_button.rb +23 -0
- data/lib/generators/wcc/templates/page/generated_add_pages.ts +50 -0
- data/lib/generators/wcc/templates/page/models/page.rb +23 -0
- data/lib/generators/wcc/templates/release +9 -0
- data/lib/generators/wcc/templates/wcc_contentful.rb +17 -0
- data/lib/wcc/contentful.rb +32 -2
- data/lib/wcc/contentful/exceptions.rb +33 -0
- data/lib/wcc/contentful/model.rb +1 -0
- data/lib/wcc/contentful/model/dropdown_menu.rb +7 -0
- data/lib/wcc/contentful/model/menu.rb +6 -0
- data/lib/wcc/contentful/model/menu_button.rb +16 -0
- data/lib/wcc/contentful/model/page.rb +8 -0
- data/lib/wcc/contentful/model/redirect.rb +19 -0
- data/lib/wcc/contentful/model_validators.rb +121 -0
- data/lib/wcc/contentful/model_validators/dsl.rb +166 -0
- data/lib/wcc/contentful/store/postgres_store.rb +2 -2
- data/lib/wcc/contentful/version.rb +1 -1
- data/wcc-contentful.gemspec +9 -3
- metadata +92 -7
@@ -0,0 +1,192 @@
|
|
1
|
+
|
2
|
+
import Migration from 'contentful-migration-cli'
|
3
|
+
|
4
|
+
export = function (migration: Migration) {
|
5
|
+
const menu = migration.createContentType('menu', {
|
6
|
+
displayField: 'name',
|
7
|
+
name: 'Menu',
|
8
|
+
description: 'A Menu contains a number of Menu Buttons or other Menus, ' +
|
9
|
+
'which will be rendered as drop-downs.'
|
10
|
+
})
|
11
|
+
|
12
|
+
menu.createField('name', {
|
13
|
+
name: 'Menu Name',
|
14
|
+
type: 'Symbol',
|
15
|
+
localized: false,
|
16
|
+
required: true,
|
17
|
+
validations: [],
|
18
|
+
disabled: false,
|
19
|
+
omitted: false
|
20
|
+
})
|
21
|
+
|
22
|
+
menu.createField('items', {
|
23
|
+
name: 'Items',
|
24
|
+
type: 'Array',
|
25
|
+
localized: false,
|
26
|
+
required: false,
|
27
|
+
validations: [],
|
28
|
+
disabled: false,
|
29
|
+
omitted: false,
|
30
|
+
items:
|
31
|
+
{
|
32
|
+
type: 'Link',
|
33
|
+
validations:
|
34
|
+
[{
|
35
|
+
linkContentType:
|
36
|
+
['dropdownMenu',
|
37
|
+
'menuButton'],
|
38
|
+
message: 'The items must be either buttons or drop-down menus.'
|
39
|
+
}],
|
40
|
+
linkType: 'Entry'
|
41
|
+
}
|
42
|
+
})
|
43
|
+
|
44
|
+
menu.changeEditorInterface('name', 'singleLine')
|
45
|
+
menu.changeEditorInterface('items', 'entryLinksEditor')
|
46
|
+
|
47
|
+
const menubutton = migration.createContentType('menuButton', {
|
48
|
+
displayField: 'text',
|
49
|
+
name: 'Menu Button',
|
50
|
+
description: 'A Menu Button is a clickable button that goes on a Menu. It has a link to a Page or a URL.'
|
51
|
+
})
|
52
|
+
|
53
|
+
menubutton.createField('text', {
|
54
|
+
name: 'Text',
|
55
|
+
type: 'Symbol',
|
56
|
+
localized: false,
|
57
|
+
required: true,
|
58
|
+
validations:
|
59
|
+
[{
|
60
|
+
size:
|
61
|
+
{
|
62
|
+
min: 1,
|
63
|
+
max: 60
|
64
|
+
},
|
65
|
+
message: 'A Menu Button should have a very short text field - ideally a single word. Please limit the text to 60 characters.'
|
66
|
+
}],
|
67
|
+
disabled: false,
|
68
|
+
omitted: false
|
69
|
+
})
|
70
|
+
|
71
|
+
menubutton.createField('icon', {
|
72
|
+
name: 'Icon',
|
73
|
+
type: 'Link',
|
74
|
+
localized: false,
|
75
|
+
required: false,
|
76
|
+
validations: [{ linkMimetypeGroup: ['image'] }],
|
77
|
+
disabled: false,
|
78
|
+
omitted: false,
|
79
|
+
linkType: 'Asset'
|
80
|
+
})
|
81
|
+
|
82
|
+
menubutton.createField('externalLink', {
|
83
|
+
name: 'External Link',
|
84
|
+
type: 'Symbol',
|
85
|
+
localized: false,
|
86
|
+
required: false,
|
87
|
+
validations:
|
88
|
+
[{
|
89
|
+
regexp: { pattern: '^(\\w+):(\\/\\/)?(\\w+:{0,1}\\w*@)?((\\w+\\.)+[^\\s\\/#]+)(:[0-9]+)?(\\/|(\\/|\\#)([\\w#!:.?+=&%@!\\-\\/]+))?$|^(\\/|(\\/|\\#)([\\w#!:.?+=&%@!\\-\\/]+))$' },
|
90
|
+
message: 'The external link must be a URL like \'https://www.watermark.org/\', a mailto url like \'mailto:info@watermark.org\', or a relative URL like \'#location-on-page\''
|
91
|
+
}],
|
92
|
+
disabled: false,
|
93
|
+
omitted: false
|
94
|
+
})
|
95
|
+
|
96
|
+
menubutton.createField('link', {
|
97
|
+
name: 'Page Link',
|
98
|
+
type: 'Link',
|
99
|
+
localized: false,
|
100
|
+
required: false,
|
101
|
+
validations:
|
102
|
+
[{
|
103
|
+
linkContentType: ['page'],
|
104
|
+
message: 'The Page Link must be a link to a Page which has a slug.'
|
105
|
+
}],
|
106
|
+
disabled: false,
|
107
|
+
omitted: false,
|
108
|
+
linkType: 'Entry'
|
109
|
+
})
|
110
|
+
|
111
|
+
menubutton.createField('ionIcon', {
|
112
|
+
name: 'Ion Icon',
|
113
|
+
type: 'Symbol',
|
114
|
+
localized: false,
|
115
|
+
required: false,
|
116
|
+
validations:
|
117
|
+
[{
|
118
|
+
regexp: { pattern: '^ion-[a-z\\-]+$' },
|
119
|
+
message: 'The icon should start with \'ion-\', like \'ion-arrow-down-c\'. See http://ionicons.com/'
|
120
|
+
}],
|
121
|
+
disabled: false,
|
122
|
+
omitted: false
|
123
|
+
})
|
124
|
+
|
125
|
+
menubutton.createField('style', {
|
126
|
+
name: 'Style',
|
127
|
+
type: 'Symbol',
|
128
|
+
localized: false,
|
129
|
+
required: false,
|
130
|
+
validations: [{ in: ['oval-border'] }],
|
131
|
+
disabled: false,
|
132
|
+
omitted: false
|
133
|
+
})
|
134
|
+
|
135
|
+
menubutton.changeEditorInterface('text', 'singleLine')
|
136
|
+
menubutton.changeEditorInterface('icon', 'assetLinkEditor')
|
137
|
+
menubutton.changeEditorInterface('externalLink', 'singleLine')
|
138
|
+
menubutton.changeEditorInterface('link', 'entryLinkEditor')
|
139
|
+
menubutton.changeEditorInterface('ionIcon', 'singleLine')
|
140
|
+
menubutton.changeEditorInterface('style', 'dropdown')
|
141
|
+
|
142
|
+
const dropdownmenu = migration.createContentType('dropdownMenu', {
|
143
|
+
displayField: 'name',
|
144
|
+
name: 'Dropdown Menu',
|
145
|
+
description: 'A Dropdown Menu can be attached to a main menu to show additional menu items on click.'
|
146
|
+
})
|
147
|
+
|
148
|
+
dropdownmenu.createField('name', {
|
149
|
+
name: 'Menu Name',
|
150
|
+
type: 'Symbol',
|
151
|
+
localized: false,
|
152
|
+
required: false,
|
153
|
+
validations: [],
|
154
|
+
disabled: false,
|
155
|
+
omitted: false
|
156
|
+
})
|
157
|
+
|
158
|
+
dropdownmenu.createField('label', {
|
159
|
+
name: 'Menu Label',
|
160
|
+
type: 'Link',
|
161
|
+
localized: false,
|
162
|
+
required: false,
|
163
|
+
validations: [{ linkContentType: ['menuButton'] }],
|
164
|
+
disabled: false,
|
165
|
+
omitted: false,
|
166
|
+
linkType: 'Entry'
|
167
|
+
})
|
168
|
+
|
169
|
+
dropdownmenu.createField('items', {
|
170
|
+
name: 'Items',
|
171
|
+
type: 'Array',
|
172
|
+
localized: false,
|
173
|
+
required: false,
|
174
|
+
validations: [],
|
175
|
+
disabled: false,
|
176
|
+
omitted: false,
|
177
|
+
items:
|
178
|
+
{
|
179
|
+
type: 'Link',
|
180
|
+
validations:
|
181
|
+
[{
|
182
|
+
linkContentType:
|
183
|
+
['menuButton']
|
184
|
+
}],
|
185
|
+
linkType: 'Entry'
|
186
|
+
}
|
187
|
+
})
|
188
|
+
|
189
|
+
dropdownmenu.changeEditorInterface('name', 'singleLine')
|
190
|
+
dropdownmenu.changeEditorInterface('label', 'entryLinkEditor')
|
191
|
+
dropdownmenu.changeEditorInterface('items', 'entryLinksEditor')
|
192
|
+
}
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This model represents the 'menu' content type in Contentful. Any linked
|
4
|
+
# entries of the 'menu' content type will be resolved as instances of this class.
|
5
|
+
# It exposes #find, #find_by, and #find_all methods to query Contentful.
|
6
|
+
class Menu < WCC::Contentful::Model::Menu
|
7
|
+
# Add custom validations to ensure that app-specific properties exist:
|
8
|
+
# validate_field :foo, :String, :required
|
9
|
+
# validate_field :bar_links, :Array, link_to: %w[bar baz]
|
10
|
+
|
11
|
+
# Override functionality or add utilities
|
12
|
+
#
|
13
|
+
# # Example: override equality
|
14
|
+
# def ===(other)
|
15
|
+
# ...
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# # Example: override "name" attribute to always be camelized.
|
19
|
+
# # `@name` is populated by the gem in the initializer.
|
20
|
+
# def name
|
21
|
+
# @name_camelized ||= @name.camelize(true)
|
22
|
+
# end
|
23
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This model represents the 'menuButton' content type in Contentful. Any linked
|
4
|
+
# entries of the 'menuButton' content type will be resolved as instances of this class.
|
5
|
+
# It exposes #find, #find_by, and #find_all methods to query Contentful.
|
6
|
+
class MenuButton < WCC::Contentful::Model::MenuButton
|
7
|
+
# Add custom validations to ensure that app-specific properties exist:
|
8
|
+
# validate_field :foo, :String, :required
|
9
|
+
# validate_field :bar_links, :Array, link_to: %w[bar baz]
|
10
|
+
|
11
|
+
# Override functionality or add utilities
|
12
|
+
#
|
13
|
+
# # Example: override equality
|
14
|
+
# def ===(other)
|
15
|
+
# ...
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# # Example: override "text" attribute to always be camelized.
|
19
|
+
# # `@text` is populated by the gem in the initializer.
|
20
|
+
# def text
|
21
|
+
# @text_camelized ||= @text.camelize(true)
|
22
|
+
# end
|
23
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
import Migration from 'contentful-migration-cli'
|
2
|
+
|
3
|
+
export = function (migration: Migration) {
|
4
|
+
const page = migration.createContentType('page')
|
5
|
+
.name('Page')
|
6
|
+
.description('A page describes a collection of sections that correspond' +
|
7
|
+
'to a URL slug')
|
8
|
+
.displayField('title')
|
9
|
+
|
10
|
+
page.createField('title')
|
11
|
+
.name('Title')
|
12
|
+
.type('Symbol')
|
13
|
+
.required(true)
|
14
|
+
|
15
|
+
page.createField('slug')
|
16
|
+
.name('Slug')
|
17
|
+
.type('Symbol')
|
18
|
+
.required(true)
|
19
|
+
.validations([
|
20
|
+
{
|
21
|
+
unique: true
|
22
|
+
},
|
23
|
+
{
|
24
|
+
regexp: { pattern: "(\\/|\\/([\w#!:.?+=&%@!\\-\\/]))?$" },
|
25
|
+
message: "The slug must look like the path part of a URL and begin with a forward slash, example: '/my-page-slug'"
|
26
|
+
}
|
27
|
+
])
|
28
|
+
|
29
|
+
page.createField('sections')
|
30
|
+
.name('Sections')
|
31
|
+
.type('Array')
|
32
|
+
.items({
|
33
|
+
type: 'Link',
|
34
|
+
linkType: 'Entry'
|
35
|
+
})
|
36
|
+
|
37
|
+
page.createField('subpages')
|
38
|
+
.name('Subpages')
|
39
|
+
.type('Array')
|
40
|
+
.items({
|
41
|
+
type: 'Link',
|
42
|
+
linkType: 'Entry',
|
43
|
+
validations: [
|
44
|
+
{
|
45
|
+
linkContentType: [ 'page' ]
|
46
|
+
}
|
47
|
+
]
|
48
|
+
})
|
49
|
+
|
50
|
+
}
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This model represents the 'page' content type in Contentful. Any linked
|
4
|
+
# entries of the 'page' content type will be resolved as instances of this class.
|
5
|
+
# It exposes #find, #find_by, and #find_all methods to query Contentful.
|
6
|
+
class Page < WCC::Contentful::Model::Page
|
7
|
+
# Add custom validations to ensure that app-specific properties exist:
|
8
|
+
# validate_field :foo, :String, :required
|
9
|
+
# validate_field :bar_links, :Array, link_to: %w[bar baz]
|
10
|
+
|
11
|
+
# Override functionality or add utilities
|
12
|
+
#
|
13
|
+
# # Example: override equality
|
14
|
+
# def ===(other)
|
15
|
+
# ...
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# # Example: override "title" attribute to always be titlecase.
|
19
|
+
# # `@title` is populated by the gem in the initializer.
|
20
|
+
# def title
|
21
|
+
# @title_titlecased ||= @title.titlecase
|
22
|
+
# end
|
23
|
+
end
|
@@ -0,0 +1,17 @@
|
|
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
|
+
end
|
12
|
+
|
13
|
+
# Download content types, build models, and sync content
|
14
|
+
WCC::Contentful.init!
|
15
|
+
|
16
|
+
# Validate that models conform to a defined specification
|
17
|
+
WCC::Contentful.validate_models! unless defined?(Rails) && Rails.env.development?
|
data/lib/wcc/contentful.rb
CHANGED
@@ -12,6 +12,7 @@ require 'wcc/contentful/services'
|
|
12
12
|
require 'wcc/contentful/simple_client'
|
13
13
|
require 'wcc/contentful/store'
|
14
14
|
require 'wcc/contentful/content_type_indexer'
|
15
|
+
require 'wcc/contentful/model_validators'
|
15
16
|
require 'wcc/contentful/model'
|
16
17
|
require 'wcc/contentful/model_methods'
|
17
18
|
require 'wcc/contentful/model_singleton_methods'
|
@@ -25,8 +26,6 @@ module WCC::Contentful
|
|
25
26
|
class << self
|
26
27
|
# Gets the current configuration, after calling WCC::Contentful.configure
|
27
28
|
attr_reader :configuration
|
28
|
-
|
29
|
-
attr_reader :types
|
30
29
|
end
|
31
30
|
|
32
31
|
# Configures the WCC::Contentful gem to talk to a Contentful space.
|
@@ -53,6 +52,8 @@ module WCC::Contentful
|
|
53
52
|
def self.init!
|
54
53
|
raise ArgumentError, 'Please first call WCC:Contentful.configure' if configuration.nil?
|
55
54
|
|
55
|
+
@mutex ||= Mutex.new
|
56
|
+
|
56
57
|
# we want as much as possible the raw JSON from the API so use the management
|
57
58
|
# client if possible
|
58
59
|
client = Services.instance.management_client ||
|
@@ -74,6 +75,35 @@ module WCC::Contentful
|
|
74
75
|
|
75
76
|
WCC::Contentful::ModelBuilder.new(@types).build_models
|
76
77
|
|
78
|
+
# Extend all model types w/ validation & extra fields
|
79
|
+
@types.each_value do |t|
|
80
|
+
file = File.dirname(__FILE__) + "/contentful/model/#{t.name.underscore}.rb"
|
81
|
+
require file if File.exist?(file)
|
82
|
+
end
|
83
|
+
|
77
84
|
require_relative 'contentful/client_ext' if defined?(::Contentful)
|
78
85
|
end
|
86
|
+
|
87
|
+
# Runs validations over the content types returned from the Contentful API.
|
88
|
+
# Validations are configured on predefined model classes using the
|
89
|
+
# `validate_field` directive. Example:
|
90
|
+
# validate_field :top_button, :Link, :optional, link_to: 'menuButton'
|
91
|
+
# This results in a WCC::Contentful::ValidationError
|
92
|
+
# if the 'topButton' field in the 'menu' content type is not a link.
|
93
|
+
def self.validate_models!
|
94
|
+
# Ensure application models are loaded before we validate
|
95
|
+
Dir[Rails.root.join('app/models/**/*.rb')].each { |file| require file } if defined?(Rails)
|
96
|
+
|
97
|
+
content_types = WCC::Contentful::ModelValidators.transform_content_types_for_validation(
|
98
|
+
@content_types
|
99
|
+
)
|
100
|
+
errors = WCC::Contentful::Model.schema.call(content_types)
|
101
|
+
raise WCC::Contentful::ValidationError, errors.errors unless errors.success?
|
102
|
+
end
|
103
|
+
|
104
|
+
# TODO: https://zube.io/watermarkchurch/development/c/2234 init graphql
|
105
|
+
# def self.init_graphql!
|
106
|
+
# require 'wcc/contentful/graphql'
|
107
|
+
# etc...
|
108
|
+
# end
|
79
109
|
end
|
@@ -1,6 +1,39 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module WCC::Contentful
|
4
|
+
# Raised by {WCC::Contentful.validate_models!} if a content type in the space
|
5
|
+
# does not match the validation defined on the associated model.
|
6
|
+
class ValidationError < StandardError
|
7
|
+
Message =
|
8
|
+
Struct.new(:path, :error) do
|
9
|
+
def to_s
|
10
|
+
"#{path}: #{error}"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :errors
|
15
|
+
|
16
|
+
def initialize(errors)
|
17
|
+
@errors = ValidationError.join_msg_keys(errors)
|
18
|
+
super("Content Type Schema from Contentful failed validation!\n #{@errors.join("\n ")}")
|
19
|
+
end
|
20
|
+
|
21
|
+
# Turns the error messages hash into an array of message structs like:
|
22
|
+
# menu.fields.name.type: must be equal to String
|
23
|
+
def self.join_msg_keys(hash)
|
24
|
+
ret =
|
25
|
+
hash.map do |k, v|
|
26
|
+
if v.is_a?(Hash)
|
27
|
+
msgs = join_msg_keys(v)
|
28
|
+
msgs.map { |msg| Message.new(k.to_s + '.' + msg.path, msg.error) }
|
29
|
+
else
|
30
|
+
v.map { |msg| Message.new(k.to_s, msg) }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
ret.flatten(1)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
4
37
|
class SyncError < StandardError
|
5
38
|
end
|
6
39
|
|