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,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WCC::Contentful
4
+ class ValidationError < StandardError
5
+ Message =
6
+ Struct.new(:path, :error) do
7
+ def to_s
8
+ "#{path}: #{error}"
9
+ end
10
+ end
11
+
12
+ attr_reader :errors
13
+
14
+ def initialize(errors)
15
+ @errors = ValidationError.join_msg_keys(errors)
16
+ super("Content Type Schema from Contentful failed validation!\n #{@errors.join("\n ")}")
17
+ end
18
+
19
+ # Turns the error messages hash into an array of message structs like:
20
+ # menu.fields.name.type: must be equal to String
21
+ def self.join_msg_keys(hash)
22
+ ret =
23
+ hash.map do |k, v|
24
+ if v.is_a?(Hash)
25
+ msgs = join_msg_keys(v)
26
+ msgs.map { |msg| Message.new(k.to_s + '.' + msg.path, msg.error) }
27
+ else
28
+ v.map { |msg| Message.new(k.to_s, msg) }
29
+ end
30
+ end
31
+ ret.flatten(1)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,15 @@
1
+
2
+ # frozen_string_literal: true
3
+
4
+ gem 'graphql', '~> 1.7'
5
+ require 'graphql'
6
+
7
+ module WCC::Contentful
8
+ # This module builds a GraphQL schema out of our IndexedRepresentation.
9
+ # It is currently unused and not hooked up in the WCC::Contentful.init! method.
10
+ # TODO: https://zube.io/watermarkchurch/development/c/2234 hook it up
11
+ module Graphql
12
+ end
13
+ end
14
+
15
+ require_relative 'graphql/builder'
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'graphql'
4
+
5
+ require_relative 'types'
6
+
7
+ module WCC::Contentful::Graphql
8
+ class Builder
9
+ attr_reader :schema_types
10
+
11
+ def initialize(types, store)
12
+ @types = types
13
+ @store = store
14
+ end
15
+
16
+ def build_schema
17
+ @schema_types = build_schema_types
18
+
19
+ root_query_type = build_root_query(@schema_types)
20
+
21
+ builder = self
22
+ GraphQL::Schema.define do
23
+ query root_query_type
24
+
25
+ resolve_type ->(_type, obj, _ctx) {
26
+ content_type = WCC::Contentful::Helpers.content_type_from_raw(obj)
27
+ builder.schema_types[content_type]
28
+ }
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def build_root_query(schema_types)
35
+ store = @store
36
+
37
+ GraphQL::ObjectType.define do
38
+ name 'Query'
39
+ description 'The query root of this schema'
40
+
41
+ schema_types.each do |content_type, schema_type|
42
+ field schema_type.name.to_sym do
43
+ type schema_type
44
+ argument :id, types.ID
45
+ description "Find a #{schema_type.name} by ID"
46
+
47
+ resolve ->(_obj, args, _ctx) {
48
+ if args['id'].nil?
49
+ store.find_by(content_type: content_type).first
50
+ else
51
+ store.find(args['id'])
52
+ end
53
+ }
54
+ end
55
+
56
+ field "all#{schema_type.name}".to_sym do
57
+ type schema_type.to_list_type
58
+ argument :filter, Types::FilterType
59
+
60
+ resolve ->(_obj, args, ctx) {
61
+ relation = store.find_by(content_type: content_type)
62
+ relation = relation.apply(args[:filter], ctx) if args[:filter]
63
+ relation.result
64
+ }
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ def build_schema_types
71
+ @types.each_with_object({}) do |(k, v), h|
72
+ h[k] = build_schema_type(v)
73
+ end
74
+ end
75
+
76
+ def build_schema_type(typedef)
77
+ store = @store
78
+ builder = self
79
+ content_type = typedef.content_type
80
+
81
+ GraphQL::ObjectType.define do
82
+ name(typedef.name)
83
+
84
+ description("Generated from content type #{content_type}")
85
+
86
+ field :id, !types.ID do
87
+ resolve ->(obj, _args, _ctx) {
88
+ obj.dig('sys', 'id')
89
+ }
90
+ end
91
+
92
+ field :_content_type, !types.String do
93
+ resolve ->(_, _, _) {
94
+ content_type
95
+ }
96
+ end
97
+
98
+ # Make a field for each column:
99
+ typedef.fields.each_value do |f|
100
+ case f.type
101
+ when :Asset
102
+ field(f.name.to_sym, -> {
103
+ type = builder.schema_types['Asset']
104
+ type = type.to_list_type if f.array
105
+ type
106
+ }) do
107
+ resolve ->(obj, _args, ctx) {
108
+ links = obj.dig('fields', f.name, ctx[:locale] || 'en-US')
109
+ return if links.nil?
110
+
111
+ if links.is_a? Array
112
+ links.reject(&:nil?).map { |l| store.find(l.dig('sys', 'id')) }
113
+ else
114
+ store.find(links.dig('sys', 'id'))
115
+ end
116
+ }
117
+ end
118
+ when :Link
119
+ field(f.name.to_sym, -> {
120
+ type =
121
+ if f.link_types.nil? || f.link_types.empty?
122
+ builder.schema_types['AnyContentful'] ||=
123
+ Types::BuildUnionType.call(builder.schema_types, 'AnyContentful')
124
+ elsif f.link_types.length == 1
125
+ builder.schema_types[f.link_types.first]
126
+ else
127
+ from_types = builder.schema_types.select { |key| f.link_types.include?(key) }
128
+ name = "#{typedef.name}_#{f.name}"
129
+ builder.schema_types[name] ||= Types::BuildUnionType.call(from_types, name)
130
+ end
131
+ type = type.to_list_type if f.array
132
+ type
133
+ }) do
134
+ resolve ->(obj, _args, ctx) {
135
+ links = obj.dig('fields', f.name, ctx[:locale] || 'en-US')
136
+ return if links.nil?
137
+
138
+ if links.is_a? Array
139
+ links.reject(&:nil?).map { |l| store.find(l.dig('sys', 'id')) }
140
+ else
141
+ store.find(links.dig('sys', 'id'))
142
+ end
143
+ }
144
+ end
145
+ else
146
+ type =
147
+ case f.type
148
+ when :DateTime
149
+ Types::DateTimeType
150
+ when :Coordinates
151
+ Types::CoordinatesType
152
+ when :Json
153
+ Types::HashType
154
+ else
155
+ types.public_send(f.type)
156
+ end
157
+ type = type.to_list_type if f.array
158
+ field(f.name.to_sym, type) do
159
+ resolve ->(obj, _args, ctx) {
160
+ if obj.is_a? Array
161
+ obj.map { |o| o.dig('fields', f.name, ctx[:locale] || 'en-US') }
162
+ else
163
+ obj.dig('fields', f.name, ctx[:locale] || 'en-US')
164
+ end
165
+ }
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,54 @@
1
+
2
+ # frozen_string_literal: true
3
+
4
+ module WCC::Contentful::Graphql::Types
5
+ DateTimeType =
6
+ GraphQL::ScalarType.define do
7
+ name 'DateTime'
8
+
9
+ coerce_result ->(value, _ctx) { Time.zone.parse(value) }
10
+ end
11
+
12
+ HashType =
13
+ GraphQL::ScalarType.define do
14
+ name 'Hash'
15
+
16
+ coerce_result ->(value, _ctx) {
17
+ return value if value.is_a? Array
18
+ return value.to_h if value.respond_to?(:to_h)
19
+ return JSON.parse(value) if value.is_a? String
20
+ raise ArgumentError, "Cannot coerce value '#{value}' to a hash"
21
+ }
22
+ end
23
+
24
+ CoordinatesType =
25
+ GraphQL::ObjectType.define do
26
+ name 'Coordinates'
27
+
28
+ field :lat, !types.Float, hash_key: 'lat'
29
+ field :lon, !types.Float, hash_key: 'lon'
30
+ end
31
+
32
+ AnyScalarInputType =
33
+ GraphQL::ScalarType.define do
34
+ name 'Any'
35
+ end
36
+
37
+ FilterType =
38
+ GraphQL::InputObjectType.define do
39
+ name 'filter'
40
+
41
+ argument :field, !types.String
42
+ argument :eq, AnyScalarInputType
43
+ end
44
+
45
+ BuildUnionType =
46
+ ->(from_types, union_type_name) do
47
+ possible_types = from_types.values.reject { |t| t.is_a? GraphQL::UnionType }
48
+
49
+ GraphQL::UnionType.define do
50
+ name union_type_name
51
+ possible_types possible_types
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,28 @@
1
+
2
+ # frozen_string_literal: true
3
+
4
+ module WCC::Contentful::Helpers
5
+ extend self
6
+
7
+ def content_type_from_raw(value)
8
+ case value.dig('sys', 'type')
9
+ when 'Entry'
10
+ value.dig('sys', 'contentType', 'sys', 'id')
11
+ when 'Asset'
12
+ 'Asset'
13
+ else
14
+ raise ArgumentError, "Unknown content type '#{value.dig('sys', 'type') || 'null'}'"
15
+ end
16
+ end
17
+
18
+ def constant_from_content_type(content_type)
19
+ content_type.camelize.gsub(/[^_a-zA-Z0-9]/, '_')
20
+ end
21
+
22
+ def shared_prefix(string_array)
23
+ string_array.reduce do |l, s|
24
+ l = l.chop while l != s[0...l.length]
25
+ l
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WCC::Contentful
4
+ # The result of running the indexer on raw content types to produce
5
+ # a type definition which can be used to build models or graphql types.
6
+ class IndexedRepresentation
7
+ def initialize(types = {})
8
+ @types = types
9
+ end
10
+
11
+ delegate :keys, to: :@types
12
+ delegate :[], to: :@types
13
+ delegate :each_with_object, to: :@types
14
+ delegate :each_value, to: :@types
15
+
16
+ def []=(id, value)
17
+ raise ArgumentError unless value.is_a?(ContentType)
18
+ @types[id] = value
19
+ end
20
+
21
+ def self.from_json(hash)
22
+ hash = JSON.parse(hash) if hash.is_a?(String)
23
+
24
+ ret = IndexedRepresentation.new
25
+ hash.each do |id, content_type_hash|
26
+ ret[id] = ContentType.new(content_type_hash)
27
+ end
28
+ ret
29
+ end
30
+
31
+ def to_json
32
+ @types.to_json
33
+ end
34
+
35
+ class ContentType
36
+ ATTRIBUTES = %i[
37
+ name
38
+ content_type
39
+ fields
40
+ ].freeze
41
+
42
+ attr_accessor(*ATTRIBUTES)
43
+
44
+ def initialize(hash_or_id = nil)
45
+ @fields = {}
46
+ return unless hash_or_id
47
+ if hash_or_id.is_a?(String)
48
+ @name = hash_or_id
49
+ return
50
+ end
51
+
52
+ if raw_fields = (hash_or_id.delete('fields') || hash_or_id.delete(:fields))
53
+ raw_fields.each do |field_name, raw_field|
54
+ @fields[field_name] = Field.new(raw_field)
55
+ end
56
+ end
57
+
58
+ hash_or_id.each { |k, v| public_send("#{k}=", v) }
59
+ end
60
+ end
61
+
62
+ class Field
63
+ ATTRIBUTES = %i[
64
+ name
65
+ type
66
+ array
67
+ required
68
+ link_types
69
+ ].freeze
70
+
71
+ attr_accessor(*ATTRIBUTES)
72
+
73
+ TYPES = %i[
74
+ String
75
+ Int
76
+ Float
77
+ DateTime
78
+ Boolean
79
+ Json
80
+ Coordinates
81
+ Link
82
+ Asset
83
+ ].freeze
84
+
85
+ def type=(raw_type)
86
+ unless TYPES.include?(raw_type)
87
+ raise ArgumentError, "Unknown type #{raw_type}, expected one of: #{TYPES}"
88
+ end
89
+ @type = raw_type
90
+ end
91
+
92
+ def initialize(hash_or_id = nil)
93
+ return unless hash_or_id
94
+ if hash_or_id.is_a?(String)
95
+ @name = hash_or_id
96
+ return
97
+ end
98
+
99
+ if raw_type = hash_or_id.delete('type')
100
+ raw_type = raw_type.to_sym
101
+ unless TYPES.include?(raw_type)
102
+ raise ArgumentError, "Unknown type #{raw_type}, expected one of: #{TYPES}"
103
+ end
104
+ @type = raw_type
105
+ end
106
+
107
+ hash_or_id.each { |k, v| public_send("#{k}=", v) }
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,24 @@
1
+
2
+ # frozen_string_literal: true
3
+
4
+ class WCC::Contentful::Model
5
+ extend WCC::Contentful::Helpers
6
+ extend WCC::Contentful::ModelValidators
7
+
8
+ class << self
9
+ attr_accessor :store
10
+ end
11
+
12
+ def self.all_models
13
+ WCC::Contentful::Model.constants(false).map { |k| WCC::Contentful::Model.const_get(k) }
14
+ end
15
+
16
+ def self.find(id, context = nil)
17
+ return unless raw = store.find(id)
18
+
19
+ content_type = content_type_from_raw(raw)
20
+
21
+ const = WCC::Contentful::Model.const_get(constant_from_content_type(content_type))
22
+ const.new(raw, context)
23
+ end
24
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class WCC::Contentful::Model::Menu < WCC::Contentful::Model
4
+ validate_field :name, :String
5
+ validate_field :top_button, :Link, :optional, link_to: 'menuButton'
6
+ validate_field :items, :Array, link_to: %w[menu menuButton]
7
+ end