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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +51 -0
  3. data/.gitignore +26 -0
  4. data/.rspec +1 -0
  5. data/.rubocop.yml +242 -0
  6. data/.rubocop_todo.yml +19 -0
  7. data/.travis.yml +5 -0
  8. data/CHANGELOG.md +180 -0
  9. data/CODE_OF_CONDUCT.md +74 -0
  10. data/Guardfile +58 -0
  11. data/LICENSE.txt +21 -0
  12. data/Rakefile +8 -0
  13. data/bin/console +3 -4
  14. data/bin/rails +0 -2
  15. data/lib/generators/wcc/USAGE +24 -0
  16. data/lib/generators/wcc/model_generator.rb +90 -0
  17. data/lib/generators/wcc/templates/.keep +0 -0
  18. data/lib/generators/wcc/templates/Procfile +3 -0
  19. data/lib/generators/wcc/templates/contentful_shell_wrapper +385 -0
  20. data/lib/generators/wcc/templates/menu/generated_add_menus.ts +192 -0
  21. data/lib/generators/wcc/templates/menu/models/menu.rb +23 -0
  22. data/lib/generators/wcc/templates/menu/models/menu_button.rb +23 -0
  23. data/lib/generators/wcc/templates/page/generated_add_pages.ts +50 -0
  24. data/lib/generators/wcc/templates/page/models/page.rb +23 -0
  25. data/lib/generators/wcc/templates/release +9 -0
  26. data/lib/generators/wcc/templates/wcc_contentful.rb +17 -0
  27. data/lib/wcc/contentful.rb +32 -2
  28. data/lib/wcc/contentful/exceptions.rb +33 -0
  29. data/lib/wcc/contentful/model.rb +1 -0
  30. data/lib/wcc/contentful/model/dropdown_menu.rb +7 -0
  31. data/lib/wcc/contentful/model/menu.rb +6 -0
  32. data/lib/wcc/contentful/model/menu_button.rb +16 -0
  33. data/lib/wcc/contentful/model/page.rb +8 -0
  34. data/lib/wcc/contentful/model/redirect.rb +19 -0
  35. data/lib/wcc/contentful/model_validators.rb +121 -0
  36. data/lib/wcc/contentful/model_validators/dsl.rb +166 -0
  37. data/lib/wcc/contentful/store/postgres_store.rb +2 -2
  38. data/lib/wcc/contentful/version.rb +1 -1
  39. data/wcc-contentful.gemspec +9 -3
  40. 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,9 @@
1
+ #!/bin/sh
2
+
3
+ set -e
4
+
5
+ echo "Migrating database..."
6
+ bundle exec rake db:migrate
7
+
8
+ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
9
+ $DIR/contentful migrate -y
@@ -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?
@@ -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
 
@@ -38,6 +38,7 @@
38
38
  # @api Model
39
39
  class WCC::Contentful::Model
40
40
  extend WCC::Contentful::Helpers
41
+ extend WCC::Contentful::ModelValidators
41
42
 
42
43
  # The Model base class maintains a registry which is best expressed as a
43
44
  # class var.