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.
- checksums.yaml +4 -4
- data/.circleci/config.yml +51 -0
- data/.gitignore +8 -0
- data/.rspec +1 -0
- data/.rubocop.yml +240 -0
- data/.rubocop_todo.yml +13 -0
- data/CHANGELOG.md +7 -1
- data/Gemfile +4 -2
- data/Guardfile +36 -0
- data/README.md +1 -1
- data/Rakefile +5 -3
- data/bin/rspec +3 -0
- data/lib/generators/wcc/USAGE +24 -0
- data/lib/generators/wcc/menu_generator.rb +67 -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 +342 -0
- data/lib/generators/wcc/templates/menu/generated_add_menus.ts +85 -0
- data/lib/generators/wcc/templates/menu/menu.rb +25 -0
- data/lib/generators/wcc/templates/menu/menu_button.rb +25 -0
- data/lib/generators/wcc/templates/release +9 -0
- data/lib/generators/wcc/templates/wcc_contentful.rb +18 -0
- data/lib/wcc/contentful.rb +93 -26
- data/lib/wcc/contentful/client_ext.rb +15 -0
- data/lib/wcc/contentful/configuration.rb +93 -0
- data/lib/wcc/contentful/content_type_indexer.rb +153 -0
- data/lib/wcc/contentful/exceptions.rb +34 -0
- data/lib/wcc/contentful/graphql.rb +15 -0
- data/lib/wcc/contentful/graphql/builder.rb +172 -0
- data/lib/wcc/contentful/graphql/types.rb +54 -0
- data/lib/wcc/contentful/helpers.rb +28 -0
- data/lib/wcc/contentful/indexed_representation.rb +111 -0
- data/lib/wcc/contentful/model.rb +24 -0
- data/lib/wcc/contentful/model/menu.rb +7 -0
- data/lib/wcc/contentful/model/menu_button.rb +15 -0
- data/lib/wcc/contentful/model_builder.rb +151 -0
- data/lib/wcc/contentful/model_validators.rb +64 -0
- data/lib/wcc/contentful/model_validators/dsl.rb +165 -0
- data/lib/wcc/contentful/simple_client.rb +127 -0
- data/lib/wcc/contentful/simple_client/response.rb +160 -0
- data/lib/wcc/contentful/store.rb +8 -0
- data/lib/wcc/contentful/store/cdn_adapter.rb +79 -0
- data/lib/wcc/contentful/store/memory_store.rb +75 -0
- data/lib/wcc/contentful/store/postgres_store.rb +132 -0
- data/lib/wcc/contentful/version.rb +3 -1
- data/wcc-contentful.gemspec +49 -24
- metadata +261 -16
- data/lib/wcc/contentful/redirect.rb +0 -33
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class WCC::Contentful::Model::MenuButton < WCC::Contentful::Model
|
4
|
+
validate_field :text, :String, :required
|
5
|
+
validate_field :icon, :Asset, :optional
|
6
|
+
validate_field :external_link, :String, :optional
|
7
|
+
validate_field :link, :Link, :optional, link_to: 'page'
|
8
|
+
|
9
|
+
# Gets either the external link or the slug from the referenced page.
|
10
|
+
# Example usage: `<%= link_to button.title, button.href %>`
|
11
|
+
def href
|
12
|
+
return external_link if external_link
|
13
|
+
link&.try(:slug) || link&.try(:url)
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WCC::Contentful
|
4
|
+
class ModelBuilder
|
5
|
+
include Helpers
|
6
|
+
|
7
|
+
def initialize(types)
|
8
|
+
@types = types
|
9
|
+
end
|
10
|
+
|
11
|
+
def build_models
|
12
|
+
@types.each_with_object([]) do |(_k, v), a|
|
13
|
+
a << build_model(v)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def build_model(typedef)
|
20
|
+
const = typedef.name
|
21
|
+
return WCC::Contentful::Model.const_get(const) if WCC::Contentful::Model.const_defined?(const)
|
22
|
+
|
23
|
+
# TODO: https://github.com/dkubb/ice_nine ?
|
24
|
+
typedef = typedef.deep_dup.freeze
|
25
|
+
fields = typedef.fields.keys
|
26
|
+
WCC::Contentful::Model.const_set(const,
|
27
|
+
Class.new(WCC::Contentful::Model) do
|
28
|
+
include Helpers
|
29
|
+
|
30
|
+
define_singleton_method(:content_type) do
|
31
|
+
typedef.content_type
|
32
|
+
end
|
33
|
+
|
34
|
+
define_singleton_method(:content_type_definition) do
|
35
|
+
typedef
|
36
|
+
end
|
37
|
+
|
38
|
+
define_singleton_method(:find) do |id, context = nil|
|
39
|
+
raw = WCC::Contentful::Model.store.find(id)
|
40
|
+
new(raw, context) if raw.present?
|
41
|
+
end
|
42
|
+
|
43
|
+
define_singleton_method(:find_all) do |context = nil|
|
44
|
+
raw = WCC::Contentful::Model.store.find_by(content_type: content_type)
|
45
|
+
raw.map { |r| new(r, context) }
|
46
|
+
end
|
47
|
+
|
48
|
+
define_singleton_method(:find_by) do |filter, context = nil|
|
49
|
+
filter.transform_keys! { |k| k.to_s.camelize(:lower) }
|
50
|
+
bad_fields = filter.keys.reject { |k| fields.include?(k) }
|
51
|
+
raise ArgumentError, "These fields do not exist: #{bad_fields}" unless bad_fields.empty?
|
52
|
+
|
53
|
+
query = WCC::Contentful::Model.store.find_by(content_type: content_type)
|
54
|
+
filter.each do |field, v|
|
55
|
+
query = query.eq(field, v, context)
|
56
|
+
end
|
57
|
+
query.map { |r| new(r, context) }
|
58
|
+
end
|
59
|
+
|
60
|
+
define_method(:initialize) do |raw, context = nil|
|
61
|
+
ct = content_type_from_raw(raw)
|
62
|
+
if ct != typedef.content_type
|
63
|
+
raise ArgumentError, 'Wrong Content Type - ' \
|
64
|
+
"'#{raw.dig('sys', 'id')}' is a #{ct}, expected #{typedef.content_type}"
|
65
|
+
end
|
66
|
+
|
67
|
+
@locale = context[:locale] if context.present?
|
68
|
+
@locale ||= 'en-US'
|
69
|
+
@id = raw.dig('sys', 'id')
|
70
|
+
@space = raw.dig('sys', 'space', 'sys', 'id')
|
71
|
+
@created_at = raw.dig('sys', 'createdAt')
|
72
|
+
@created_at = Time.parse(@created_at) if @created_at.present?
|
73
|
+
@updated_at = raw.dig('sys', 'updatedAt')
|
74
|
+
@updated_at = Time.parse(@updated_at) if @updated_at.present?
|
75
|
+
@revision = raw.dig('sys', 'revision')
|
76
|
+
|
77
|
+
typedef.fields.each_value do |f|
|
78
|
+
raw_value = raw.dig('fields', f.name, @locale)
|
79
|
+
if raw_value.present?
|
80
|
+
case f.type
|
81
|
+
when :DateTime
|
82
|
+
raw_value = Time.zone.parse(raw_value)
|
83
|
+
when :Int
|
84
|
+
raw_value = Integer(raw_value)
|
85
|
+
when :Float
|
86
|
+
raw_value = Float(raw_value)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
instance_variable_set('@' + f.name, raw_value)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
attr_reader :id
|
94
|
+
attr_reader :space
|
95
|
+
attr_reader :created_at
|
96
|
+
attr_reader :updated_at
|
97
|
+
attr_reader :revision
|
98
|
+
|
99
|
+
# Make a field for each column:
|
100
|
+
typedef.fields.each_value do |f|
|
101
|
+
name = f.name
|
102
|
+
var_name = '@' + name
|
103
|
+
case f.type
|
104
|
+
when :Asset, :Link
|
105
|
+
define_method(name) do
|
106
|
+
val = instance_variable_get(var_name + '_resolved')
|
107
|
+
return val if val.present?
|
108
|
+
|
109
|
+
return unless val = instance_variable_get(var_name)
|
110
|
+
|
111
|
+
val =
|
112
|
+
if val.is_a? Array
|
113
|
+
val.map { |v| WCC::Contentful::Model.find(v.dig('sys', 'id')) }
|
114
|
+
else
|
115
|
+
WCC::Contentful::Model.find(val.dig('sys', 'id'))
|
116
|
+
end
|
117
|
+
|
118
|
+
instance_variable_set(var_name + '_resolved', val)
|
119
|
+
val
|
120
|
+
end
|
121
|
+
when :Coordinates
|
122
|
+
define_method(name) do
|
123
|
+
val = instance_variable_get(var_name)
|
124
|
+
OpenStruct.new(val.slice('lat', 'lon')) if val
|
125
|
+
end
|
126
|
+
when :Json
|
127
|
+
define_method(name) do
|
128
|
+
value = instance_variable_get(var_name)
|
129
|
+
|
130
|
+
parse_value =
|
131
|
+
->(v) do
|
132
|
+
return v.to_h if v.respond_to?(:to_h)
|
133
|
+
|
134
|
+
raise ArgumentError, "Cannot coerce value '#{value}' to a hash"
|
135
|
+
end
|
136
|
+
|
137
|
+
return value.map { |v| OpenStruct.new(parse_value.call(v)) } if value.is_a?(Array)
|
138
|
+
|
139
|
+
OpenStruct.new(parse_value.call(value))
|
140
|
+
end
|
141
|
+
else
|
142
|
+
define_method(name) do
|
143
|
+
instance_variable_get(var_name)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
alias_method name.underscore, name
|
147
|
+
end
|
148
|
+
end)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry-validation'
|
4
|
+
|
5
|
+
require_relative 'model_validators/dsl'
|
6
|
+
|
7
|
+
module WCC::Contentful::ModelValidators
|
8
|
+
def schema
|
9
|
+
return if @field_validations.nil? || @field_validations.empty?
|
10
|
+
field_validations = @field_validations
|
11
|
+
|
12
|
+
# "page": {
|
13
|
+
# "sys": { ... }
|
14
|
+
# "fields": {
|
15
|
+
# "title": { ... },
|
16
|
+
# "sections": { ... },
|
17
|
+
# ...
|
18
|
+
# }
|
19
|
+
# }
|
20
|
+
|
21
|
+
fields_schema =
|
22
|
+
Dry::Validation.Schema do
|
23
|
+
# Had to dig through the internals of Dry::Validation to find
|
24
|
+
# this magic incantation
|
25
|
+
field_validations.each { |dsl| instance_eval(&dsl.to_proc) }
|
26
|
+
end
|
27
|
+
|
28
|
+
Dry::Validation.Schema do
|
29
|
+
required('fields').schema(fields_schema)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def validate_fields(&block)
|
34
|
+
raise ArgumentError, 'validate_fields requires a block' unless block_given?
|
35
|
+
dsl = ProcDsl.new(Proc.new(&block))
|
36
|
+
|
37
|
+
(@field_validations ||= []) << dsl
|
38
|
+
end
|
39
|
+
|
40
|
+
def validate_field(field, type, *options)
|
41
|
+
dsl = FieldDsl.new(field, type, options)
|
42
|
+
|
43
|
+
(@field_validations ||= []) << dsl
|
44
|
+
end
|
45
|
+
|
46
|
+
# Accepts a content types response from the API and transforms it
|
47
|
+
# to be acceptible for the validator.
|
48
|
+
def self.transform_content_types_for_validation(content_types)
|
49
|
+
if !content_types.is_a?(Array) && items = content_types.try(:[], 'items')
|
50
|
+
content_types = items
|
51
|
+
end
|
52
|
+
|
53
|
+
# Transform the array into a hash keyed by content type ID
|
54
|
+
content_types.each_with_object({}) do |ct, ct_hash|
|
55
|
+
# Transform the fields into a hash keyed by field ID
|
56
|
+
ct['fields'] =
|
57
|
+
ct['fields'].each_with_object({}) do |f, f_hash|
|
58
|
+
f_hash[f['id']] = f
|
59
|
+
end
|
60
|
+
|
61
|
+
ct_hash[ct.dig('sys', 'id')] = ct
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WCC::Contentful::ModelValidators
|
4
|
+
class ProcDsl
|
5
|
+
def to_proc
|
6
|
+
@proc
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(proc)
|
10
|
+
@proc = proc
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class FieldDsl
|
15
|
+
attr_reader :field
|
16
|
+
|
17
|
+
# "sections": {
|
18
|
+
# "id": "sections",
|
19
|
+
# "name": "Sections",
|
20
|
+
# "type": "Array",
|
21
|
+
# "localized": false,
|
22
|
+
# "required": false,
|
23
|
+
# "validations": [],
|
24
|
+
# "disabled": false,
|
25
|
+
# "omitted": false,
|
26
|
+
# "items": {
|
27
|
+
# "type": "Link",
|
28
|
+
# "validations": [
|
29
|
+
# {
|
30
|
+
# "linkContentType": [
|
31
|
+
# "Section"
|
32
|
+
# ]
|
33
|
+
# }
|
34
|
+
# ],
|
35
|
+
# "linkType": "Entry"
|
36
|
+
# }
|
37
|
+
# }
|
38
|
+
|
39
|
+
def schema
|
40
|
+
return @field_schema if @field_schema
|
41
|
+
|
42
|
+
# example: required('type').value(...)
|
43
|
+
type_pred = parse_type_predicate(@type)
|
44
|
+
|
45
|
+
# example: [required('required').value(eq?: true), ...]
|
46
|
+
procs =
|
47
|
+
@options.map do |opt|
|
48
|
+
if opt.is_a?(Hash)
|
49
|
+
opt.map { |k, v| parse_option(k, v) }
|
50
|
+
else
|
51
|
+
parse_option(opt)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
@field_schema =
|
56
|
+
Dry::Validation.Schema do
|
57
|
+
instance_eval(&type_pred)
|
58
|
+
|
59
|
+
procs.flatten.each { |p| instance_eval(&p) }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def to_proc
|
64
|
+
f = field
|
65
|
+
s = schema
|
66
|
+
proc { required(f).schema(s) }
|
67
|
+
end
|
68
|
+
|
69
|
+
def initialize(field, field_type, options)
|
70
|
+
@field = field.to_s.camelize(:lower) unless field.is_a?(String)
|
71
|
+
@type = field_type
|
72
|
+
@options = options
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def parse_type_predicate(type)
|
78
|
+
case type
|
79
|
+
when :String
|
80
|
+
proc { required('type').value(included_in?: %w[Symbol Text]) }
|
81
|
+
when :Int
|
82
|
+
proc { required('type').value(eql?: 'Integer') }
|
83
|
+
when :Float
|
84
|
+
proc { required('type').value(eql?: 'Number') }
|
85
|
+
when :DateTime
|
86
|
+
proc { required('type').value(eql?: 'Date') }
|
87
|
+
when :Asset
|
88
|
+
proc {
|
89
|
+
required('type').value(eql?: 'Link')
|
90
|
+
required('linkType').value(eql?: 'Asset')
|
91
|
+
}
|
92
|
+
else
|
93
|
+
proc { required('type').value(eql?: type.to_s.camelize) }
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def parse_option(option, option_arg = nil)
|
98
|
+
case option
|
99
|
+
when :required
|
100
|
+
proc { required('required').value(eql?: true) }
|
101
|
+
when :optional
|
102
|
+
proc { required('required').value(eql?: false) }
|
103
|
+
when :link_to
|
104
|
+
link_to_proc = parse_field_link_to(option_arg)
|
105
|
+
return link_to_proc unless @type.to_s.camelize == 'Array'
|
106
|
+
proc {
|
107
|
+
required('items').schema do
|
108
|
+
required('type').value(eql?: 'Link')
|
109
|
+
instance_eval(&link_to_proc)
|
110
|
+
end
|
111
|
+
}
|
112
|
+
when :items
|
113
|
+
type_pred = parse_type_predicate(option_arg)
|
114
|
+
proc {
|
115
|
+
required('items').schema do
|
116
|
+
instance_eval(&type_pred)
|
117
|
+
end
|
118
|
+
}
|
119
|
+
else
|
120
|
+
raise ArgumentError, "unknown validation requirement: #{option}"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def parse_field_link_to(option_arg)
|
125
|
+
raise ArgumentError, 'validation link_to: requires an argument' unless option_arg
|
126
|
+
|
127
|
+
# this works because a Link can only have one validation in its "validations" array -
|
128
|
+
# this will fail if Contentful ever changes that.
|
129
|
+
|
130
|
+
# the 'validations' schema needs to be optional because if we get the content
|
131
|
+
# types from the CDN instead of the management API, sometimes the validations
|
132
|
+
# don't get sent back.
|
133
|
+
|
134
|
+
# "validations": [
|
135
|
+
# {
|
136
|
+
# "linkContentType": [
|
137
|
+
# "section-CardSearch",
|
138
|
+
# "section-Faq",
|
139
|
+
# "section-Testimonials",
|
140
|
+
# "section-VideoHighlight"
|
141
|
+
# ]
|
142
|
+
# }
|
143
|
+
# ]
|
144
|
+
|
145
|
+
if option_arg.is_a?(Regexp)
|
146
|
+
return proc {
|
147
|
+
optional('validations').each do
|
148
|
+
schema do
|
149
|
+
required('linkContentType').each(format?: option_arg)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
}
|
153
|
+
end
|
154
|
+
|
155
|
+
option_arg = [option_arg] unless option_arg.is_a?(Array)
|
156
|
+
proc {
|
157
|
+
optional('validations').each do
|
158
|
+
schema do
|
159
|
+
required('linkContentType').value(eql?: option_arg)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
}
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'http'
|
4
|
+
|
5
|
+
require_relative 'simple_client/response'
|
6
|
+
|
7
|
+
module WCC::Contentful
|
8
|
+
class SimpleClient
|
9
|
+
def initialize(api_url:, space:, access_token:, **options)
|
10
|
+
@api_url = URI.join(api_url, '/spaces/', space + '/')
|
11
|
+
@space = space
|
12
|
+
@access_token = access_token
|
13
|
+
|
14
|
+
@get_http = options[:override_get_http] if options[:override_get_http].present?
|
15
|
+
|
16
|
+
@options = options
|
17
|
+
@query_defaults = {}
|
18
|
+
@query_defaults[:locale] = @options[:default_locale] if @options[:default_locale]
|
19
|
+
end
|
20
|
+
|
21
|
+
def get(path, query = {})
|
22
|
+
url = URI.join(@api_url, path)
|
23
|
+
|
24
|
+
Response.new(self,
|
25
|
+
{ url: url, query: query },
|
26
|
+
get_http(url, query))
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def get_http(url, query, headers = {}, proxy = {})
|
32
|
+
headers = {
|
33
|
+
Authorization: "Bearer #{@access_token}"
|
34
|
+
}.merge(headers || {})
|
35
|
+
|
36
|
+
q = @query_defaults.dup
|
37
|
+
q = q.merge(query) if query
|
38
|
+
|
39
|
+
resp =
|
40
|
+
if @get_http
|
41
|
+
@get_http.call(url, q, headers, proxy)
|
42
|
+
else
|
43
|
+
default_get_http(url, q, headers, proxy)
|
44
|
+
end
|
45
|
+
if [301, 302, 307].include?(resp.code) && !@options[:no_follow_redirects]
|
46
|
+
resp = get_http(resp.headers['location'], nil, headers, proxy)
|
47
|
+
end
|
48
|
+
resp
|
49
|
+
end
|
50
|
+
|
51
|
+
def default_get_http(url, query, headers = {}, proxy = {})
|
52
|
+
if proxy[:host]
|
53
|
+
HTTP[headers].via(proxy[:host], proxy[:port], proxy[:username], proxy[:password])
|
54
|
+
.get(url, params: query)
|
55
|
+
else
|
56
|
+
HTTP[headers].get(url, params: query)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class Cdn < SimpleClient
|
61
|
+
def initialize(space:, access_token:, **options)
|
62
|
+
super(
|
63
|
+
api_url: options[:api_url] || 'https://cdn.contentful.com/',
|
64
|
+
space: space,
|
65
|
+
access_token: access_token,
|
66
|
+
**options
|
67
|
+
)
|
68
|
+
end
|
69
|
+
|
70
|
+
def entry(key, query = {})
|
71
|
+
resp = get("entries/#{key}", query)
|
72
|
+
resp.assert_ok!
|
73
|
+
end
|
74
|
+
|
75
|
+
def entries(query = {})
|
76
|
+
resp = get('entries', query)
|
77
|
+
resp.assert_ok!
|
78
|
+
end
|
79
|
+
|
80
|
+
def asset(key, query = {})
|
81
|
+
resp = get("assets/#{key}", query)
|
82
|
+
resp.assert_ok!
|
83
|
+
end
|
84
|
+
|
85
|
+
def assets(query = {})
|
86
|
+
resp = get('assets', query)
|
87
|
+
resp.assert_ok!
|
88
|
+
end
|
89
|
+
|
90
|
+
def content_types(query = {})
|
91
|
+
resp = get('content_types', query)
|
92
|
+
resp.assert_ok!
|
93
|
+
end
|
94
|
+
|
95
|
+
def sync(sync_token: nil, **query)
|
96
|
+
sync_token =
|
97
|
+
if sync_token
|
98
|
+
{ sync_token: sync_token }
|
99
|
+
else
|
100
|
+
{ initial: true }
|
101
|
+
end
|
102
|
+
query = query.merge(sync_token)
|
103
|
+
resp = SyncResponse.new(get('sync', query))
|
104
|
+
resp.assert_ok!
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
class Management < SimpleClient
|
109
|
+
def initialize(management_token:, **options)
|
110
|
+
super(
|
111
|
+
api_url: options[:api_url] || 'https://api.contentful.com',
|
112
|
+
space: options[:space] || '/',
|
113
|
+
access_token: management_token,
|
114
|
+
**options
|
115
|
+
)
|
116
|
+
end
|
117
|
+
|
118
|
+
def content_types(space: nil, **query)
|
119
|
+
space ||= @space
|
120
|
+
raise ArgumentError, 'please provide a space ID' if space.nil?
|
121
|
+
|
122
|
+
resp = get("/spaces/#{space}/content_types", query)
|
123
|
+
resp.assert_ok!
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|