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.
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