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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +51 -0
  3. data/.gitignore +8 -0
  4. data/.rspec +1 -0
  5. data/.rubocop.yml +240 -0
  6. data/.rubocop_todo.yml +13 -0
  7. data/CHANGELOG.md +7 -1
  8. data/Gemfile +4 -2
  9. data/Guardfile +36 -0
  10. data/README.md +1 -1
  11. data/Rakefile +5 -3
  12. data/bin/rspec +3 -0
  13. data/lib/generators/wcc/USAGE +24 -0
  14. data/lib/generators/wcc/menu_generator.rb +67 -0
  15. data/lib/generators/wcc/templates/.keep +0 -0
  16. data/lib/generators/wcc/templates/Procfile +3 -0
  17. data/lib/generators/wcc/templates/contentful_shell_wrapper +342 -0
  18. data/lib/generators/wcc/templates/menu/generated_add_menus.ts +85 -0
  19. data/lib/generators/wcc/templates/menu/menu.rb +25 -0
  20. data/lib/generators/wcc/templates/menu/menu_button.rb +25 -0
  21. data/lib/generators/wcc/templates/release +9 -0
  22. data/lib/generators/wcc/templates/wcc_contentful.rb +18 -0
  23. data/lib/wcc/contentful.rb +93 -26
  24. data/lib/wcc/contentful/client_ext.rb +15 -0
  25. data/lib/wcc/contentful/configuration.rb +93 -0
  26. data/lib/wcc/contentful/content_type_indexer.rb +153 -0
  27. data/lib/wcc/contentful/exceptions.rb +34 -0
  28. data/lib/wcc/contentful/graphql.rb +15 -0
  29. data/lib/wcc/contentful/graphql/builder.rb +172 -0
  30. data/lib/wcc/contentful/graphql/types.rb +54 -0
  31. data/lib/wcc/contentful/helpers.rb +28 -0
  32. data/lib/wcc/contentful/indexed_representation.rb +111 -0
  33. data/lib/wcc/contentful/model.rb +24 -0
  34. data/lib/wcc/contentful/model/menu.rb +7 -0
  35. data/lib/wcc/contentful/model/menu_button.rb +15 -0
  36. data/lib/wcc/contentful/model_builder.rb +151 -0
  37. data/lib/wcc/contentful/model_validators.rb +64 -0
  38. data/lib/wcc/contentful/model_validators/dsl.rb +165 -0
  39. data/lib/wcc/contentful/simple_client.rb +127 -0
  40. data/lib/wcc/contentful/simple_client/response.rb +160 -0
  41. data/lib/wcc/contentful/store.rb +8 -0
  42. data/lib/wcc/contentful/store/cdn_adapter.rb +79 -0
  43. data/lib/wcc/contentful/store/memory_store.rb +75 -0
  44. data/lib/wcc/contentful/store/postgres_store.rb +132 -0
  45. data/lib/wcc/contentful/version.rb +3 -1
  46. data/wcc-contentful.gemspec +49 -24
  47. metadata +261 -16
  48. 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,9 @@
1
+ #!/bin/bash
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,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?
@@ -1,38 +1,105 @@
1
- require "wcc/contentful/version"
2
- require 'contentful_model'
1
+ # frozen_string_literal: true
3
2
 
4
- module WCC
5
- module Contentful
3
+ require 'wcc/contentful/version'
6
4
 
7
- class << self
8
- attr_accessor :configuration
9
- end
5
+ require 'active_support'
6
+ require 'active_support/core_ext/object'
10
7
 
11
- def self.configure
12
- self.configuration ||= Configuration.new
13
- yield(configuration)
14
- Configuration.configure_contentful_model
15
- end
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
- class Configuration
18
- attr_accessor :access_token, :space, :default_locale
31
+ configuration.configure_contentful
19
32
 
20
- def initialize
21
- @access_token = ""
22
- @space = ""
23
- @default_locale = ""
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
- def self.configure_contentful_model
27
- ContentfulModel.configure do |config|
28
- config.access_token = WCC::Contentful.configuration.access_token
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
- require "wcc/contentful/redirect"
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