wcc-contentful 0.1.0 → 0.2.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +1 -1
  3. data/.gitignore +5 -0
  4. data/.rubocop.yml +3 -0
  5. data/CHANGELOG.md +8 -1
  6. data/Guardfile +23 -1
  7. data/app/controllers/wcc/contentful/application_controller.rb +7 -0
  8. data/app/controllers/wcc/contentful/webhook_controller.rb +30 -0
  9. data/app/jobs/wcc/contentful/delayed_sync_job.rb +14 -0
  10. data/bin/rails +14 -0
  11. data/config/initializers/mime_types.rb +3 -0
  12. data/config/routes.rb +5 -0
  13. data/lib/generators/wcc/menu_generator.rb +4 -4
  14. data/lib/generators/wcc/templates/contentful_shell_wrapper +109 -68
  15. data/lib/generators/wcc/templates/menu/menu.rb +4 -6
  16. data/lib/generators/wcc/templates/menu/menu_button.rb +4 -6
  17. data/lib/generators/wcc/templates/release +2 -2
  18. data/lib/generators/wcc/templates/wcc_contentful.rb +0 -1
  19. data/lib/wcc/contentful/client_ext.rb +1 -1
  20. data/lib/wcc/contentful/configuration.rb +76 -35
  21. data/lib/wcc/contentful/engine.rb +13 -0
  22. data/lib/wcc/contentful/exceptions.rb +6 -0
  23. data/lib/wcc/contentful/graphql/builder.rb +8 -3
  24. data/lib/wcc/contentful/helpers.rb +6 -0
  25. data/lib/wcc/contentful/indexed_representation.rb +31 -0
  26. data/lib/wcc/contentful/model.rb +82 -1
  27. data/lib/wcc/contentful/model_builder.rb +18 -8
  28. data/lib/wcc/contentful/model_validators.rb +69 -18
  29. data/lib/wcc/contentful/simple_client/http_adapter.rb +15 -0
  30. data/lib/wcc/contentful/simple_client/typhoeus_adapter.rb +30 -0
  31. data/lib/wcc/contentful/simple_client.rb +67 -18
  32. data/lib/wcc/contentful/store/base.rb +89 -0
  33. data/lib/wcc/contentful/store/cdn_adapter.rb +13 -19
  34. data/lib/wcc/contentful/store/lazy_cache_store.rb +76 -0
  35. data/lib/wcc/contentful/store/memory_store.rb +17 -23
  36. data/lib/wcc/contentful/store/postgres_store.rb +32 -19
  37. data/lib/wcc/contentful/store.rb +62 -0
  38. data/lib/wcc/contentful/version.rb +1 -1
  39. data/lib/wcc/contentful.rb +113 -24
  40. data/wcc-contentful.gemspec +4 -0
  41. metadata +75 -2
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'http'
4
-
5
3
  class WCC::Contentful::Configuration
6
4
  ATTRIBUTES = %i[
7
5
  access_token
@@ -9,47 +7,79 @@ class WCC::Contentful::Configuration
9
7
  space
10
8
  default_locale
11
9
  content_delivery
12
- override_get_http
10
+ http_adapter
11
+ sync_cache_store
12
+ webhook_username
13
+ webhook_password
13
14
  ].freeze
14
15
  attr_accessor(*ATTRIBUTES)
15
16
 
16
- CDN_METHODS = [
17
- :eager_sync,
18
- # TODO: :lazy_sync
19
- :direct
20
- ].freeze
17
+ ##
18
+ # Defines the method by which content is downloaded from the Contentful CDN.
19
+ #
20
+ # [:direct] `config.content_delivery = :direct`
21
+ # with the `:direct` method, all queries result in web requests to
22
+ # 'https://cdn.contentful.com' via the
23
+ # {SimpleClient}[rdoc-ref:WCC::Contentful::SimpleClient::Cdn]
24
+ #
25
+ # [:eager_sync] `config.content_delivery = :eager_sync, [sync_store], [options]`
26
+ # with the `:eager_sync` method, the entire content of the Contentful
27
+ # space is downloaded locally and stored in the
28
+ # {Sync Store}[rdoc-ref:WCC::Contentful.store]. The application is responsible
29
+ # to periodically call `WCC::Contentful.sync!` to keep the store updated.
30
+ # Alternatively, the provided {Engine}[WCC::Contentful::Engine]
31
+ # can be mounted to receive a webhook from the Contentful space
32
+ # on publish events:
33
+ # mount WCC::Contentful::Engine, at: '/wcc/contentful'
34
+ #
35
+ # [:lazy_sync] `config.content_delivery = :lazy_sync, [cache]`
36
+ # The `:lazy_sync` method is a hybrid between the other two methods.
37
+ # Frequently accessed data is stored in an ActiveSupport::Cache implementation
38
+ # and is kept up-to-date via the Sync API. Any data that is not present
39
+ # in the cache is fetched from the CDN like in the `:direct` method.
40
+ # The application is still responsible to periodically call `sync!`
41
+ # or to mount the provided Engine.
42
+ #
43
+ def content_delivery=(params)
44
+ cd, *cd_params = params
45
+ unless cd.is_a? Symbol
46
+ raise ArgumentError, 'content_delivery must be a symbol, use store= to '\
47
+ 'directly set contentful CDN access adapter'
48
+ end
21
49
 
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
50
+ WCC::Contentful::Store::Factory.new(
51
+ self,
52
+ cd,
53
+ cd_params
54
+ ).validate!
29
55
 
30
- def content_delivery=(symbol)
31
- raise ArgumentError, "Please set one of #{CDN_METHODS}" unless CDN_METHODS.include?(symbol)
32
- @content_delivery = symbol
56
+ @content_delivery = cd
57
+ @content_delivery_params = cd_params
33
58
  end
34
59
 
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
60
+ ##
61
+ # Initializes the configured Sync Store.
62
+ def store
63
+ @store ||= WCC::Contentful::Store::Factory.new(
64
+ self,
65
+ @content_delivery,
66
+ @content_delivery_params
67
+ ).build_sync_store
42
68
  end
43
69
 
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
70
+ ##
71
+ # Directly sets the adapter layer for communicating with Contentful
72
+ def store=(value)
73
+ @content_delivery = :custom
74
+ @store = value
47
75
  end
48
76
 
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
77
+ # Sets the adapter which is used to make HTTP requests.
78
+ # If left unset, the gem attempts to load either 'http' or 'typhoeus'.
79
+ # You can pass your own adapter which responds to 'call', or even a lambda
80
+ # that accepts the following parameters:
81
+ # ->(url, query, headers = {}, proxy = {}) { ... }
82
+ attr_writer :http_adapter
53
83
 
54
84
  def initialize
55
85
  @access_token = ''
@@ -57,12 +87,21 @@ class WCC::Contentful::Configuration
57
87
  @space = ''
58
88
  @default_locale = nil
59
89
  @content_delivery = :direct
60
- @sync_store = :memory
61
90
  end
62
91
 
92
+ ##
93
+ # Gets a {CDN Client}[rdoc-ref:WCC::Contentful::SimpleClient::Cdn] which provides
94
+ # methods for getting and paging raw JSON data from the Contentful CDN.
63
95
  attr_reader :client
64
96
  attr_reader :management_client
65
97
 
98
+ ##
99
+ # Called by WCC::Contentful.init! to configure the
100
+ # Contentful clients. This method can be called independently of `init!` if
101
+ # the application would prefer not to generate all the models.
102
+ #
103
+ # If the {contentful.rb}[https://github.com/contentful/contentful.rb] gem is
104
+ # loaded, it is extended to make use of the `http_adapter` lambda.
66
105
  def configure_contentful
67
106
  @client = nil
68
107
  @management_client = nil
@@ -81,13 +120,15 @@ class WCC::Contentful::Configuration
81
120
  @client = WCC::Contentful::SimpleClient::Cdn.new(
82
121
  access_token: access_token,
83
122
  space: space,
84
- default_locale: default_locale
123
+ default_locale: default_locale,
124
+ adapter: http_adapter
85
125
  )
86
126
  return unless management_token.present?
87
127
  @management_client = WCC::Contentful::SimpleClient::Management.new(
88
128
  management_token: management_token,
89
129
  space: space,
90
- default_locale: default_locale
130
+ default_locale: default_locale,
131
+ adapter: http_adapter
91
132
  )
92
133
  end
93
134
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'wcc/rails'
4
+
5
+ module WCC::Contentful
6
+ class Engine < ::Rails::Engine
7
+ isolate_namespace WCC::Contentful
8
+
9
+ config.generators do |g|
10
+ g.test_framework :rspec, fixture: false
11
+ end
12
+ end
13
+ end
@@ -31,4 +31,10 @@ module WCC::Contentful
31
31
  ret.flatten(1)
32
32
  end
33
33
  end
34
+
35
+ class SyncError < StandardError
36
+ end
37
+
38
+ class ContentTypeNotFoundError < NameError
39
+ end
34
40
  end
@@ -46,7 +46,7 @@ module WCC::Contentful::Graphql
46
46
 
47
47
  resolve ->(_obj, args, _ctx) {
48
48
  if args['id'].nil?
49
- store.find_by(content_type: content_type).first
49
+ store.find_by(content_type: content_type)
50
50
  else
51
51
  store.find(args['id'])
52
52
  end
@@ -58,8 +58,13 @@ module WCC::Contentful::Graphql
58
58
  argument :filter, Types::FilterType
59
59
 
60
60
  resolve ->(_obj, args, ctx) {
61
- relation = store.find_by(content_type: content_type)
62
- relation = relation.apply(args[:filter], ctx) if args[:filter]
61
+ relation = store.find_all(content_type: content_type)
62
+ # TODO: improve this POC
63
+ if args[:filter]
64
+ filter = {}
65
+ filter[args[:filter]['field']] = { eq: args[:filter][:eq] }
66
+ relation = relation.apply(filter, ctx)
67
+ end
63
68
  relation.result
64
69
  }
65
70
  end
@@ -25,4 +25,10 @@ module WCC::Contentful::Helpers
25
25
  l
26
26
  end
27
27
  end
28
+
29
+ def content_type_from_constant(const)
30
+ return const.content_type if const.respond_to?(:content_type)
31
+ name = const.try(:name) || const.to_s
32
+ name.demodulize.camelize(:lower)
33
+ end
28
34
  end
@@ -32,6 +32,16 @@ module WCC::Contentful
32
32
  @types.to_json
33
33
  end
34
34
 
35
+ def deep_dup
36
+ self.class.new(@types.deep_dup)
37
+ end
38
+
39
+ def ==(other)
40
+ my_keys = keys
41
+ return false unless my_keys == other.keys
42
+ my_keys.all? { |k| self[k] == other[k] }
43
+ end
44
+
35
45
  class ContentType
36
46
  ATTRIBUTES = %i[
37
47
  name
@@ -57,6 +67,18 @@ module WCC::Contentful
57
67
 
58
68
  hash_or_id.each { |k, v| public_send("#{k}=", v) }
59
69
  end
70
+
71
+ def deep_dup
72
+ dup_hash =
73
+ ATTRIBUTES.each_with_object({}) do |att, h|
74
+ h[att] = public_send(att)
75
+ end
76
+ self.class.new(dup_hash)
77
+ end
78
+
79
+ def ==(other)
80
+ ATTRIBUTES.all? { |att| public_send(att) == other.public_send(att) }
81
+ end
60
82
  end
61
83
 
62
84
  class Field
@@ -96,6 +118,11 @@ module WCC::Contentful
96
118
  return
97
119
  end
98
120
 
121
+ unless hash_or_id.is_a?(Hash)
122
+ ATTRIBUTES.each { |att| public_send("#{att}=", hash_or_id.public_send(att)) }
123
+ return
124
+ end
125
+
99
126
  if raw_type = hash_or_id.delete('type')
100
127
  raw_type = raw_type.to_sym
101
128
  unless TYPES.include?(raw_type)
@@ -106,6 +133,10 @@ module WCC::Contentful
106
133
 
107
134
  hash_or_id.each { |k, v| public_send("#{k}=", v) }
108
135
  end
136
+
137
+ def ==(other)
138
+ ATTRIBUTES.all? { |att| public_send(att) == other.public_send(att) }
139
+ end
109
140
  end
110
141
  end
111
142
  end
@@ -5,20 +5,101 @@ class WCC::Contentful::Model
5
5
  extend WCC::Contentful::Helpers
6
6
  extend WCC::Contentful::ModelValidators
7
7
 
8
+ # The Model base class maintains a registry which is best expressed as a
9
+ # class var.
10
+ # rubocop:disable Style/ClassVars
11
+
8
12
  class << self
13
+ ##
14
+ # The configured store which executes all model queries against either the
15
+ # Contentful CDN or a locally-downloaded copy.
16
+ #
17
+ # See the {sync_store}[rdoc-ref:WCC::Contentful::Configuration.sync_store] parameter
18
+ # on the WCC::Contentful::Configuration class.
9
19
  attr_accessor :store
20
+
21
+ def const_missing(name)
22
+ raise WCC::Contentful::ContentTypeNotFoundError,
23
+ "Content type '#{content_type_from_constant(name)}' does not exist in the space"
24
+ end
10
25
  end
11
26
 
27
+ @@registry = {}
28
+
12
29
  def self.all_models
30
+ # TODO: this needs to use the registry but it's OK for now cause we only
31
+ # use it in specs
13
32
  WCC::Contentful::Model.constants(false).map { |k| WCC::Contentful::Model.const_get(k) }
14
33
  end
15
34
 
35
+ ##
36
+ # Finds an Entry or Asset by ID in the configured contentful space
37
+ # and returns an initialized instance of the appropriate model type.
38
+ #
39
+ # Makes use of the configured {store}[rdoc-ref:WCC::Contentful::Model.store]
40
+ # to access the Contentful CDN.
16
41
  def self.find(id, context = nil)
17
42
  return unless raw = store.find(id)
18
43
 
19
44
  content_type = content_type_from_raw(raw)
20
45
 
21
- const = WCC::Contentful::Model.const_get(constant_from_content_type(content_type))
46
+ unless const = @@registry[content_type]
47
+ begin
48
+ # The app may have defined a model and we haven't loaded it yet
49
+ const = Object.const_missing(constant_from_content_type(content_type).to_s)
50
+ rescue NameError
51
+ nil
52
+ end
53
+ end
54
+ unless const
55
+ # Autoloading couldn't find their model - we'll register our own.
56
+ const = WCC::Contentful::Model.const_get(constant_from_content_type(content_type))
57
+ register_for_content_type(content_type, klass: const)
58
+ end
59
+
22
60
  const.new(raw, context)
23
61
  end
62
+
63
+ ##
64
+ # Registers a class constant to be instantiated when resolving an instance
65
+ # of the given content type. This automatically happens for the first subclass
66
+ # of a generated model type, example:
67
+ #
68
+ # class MyMenu < WCC::Contentful::Model::Menu
69
+ # end
70
+ #
71
+ # In the above case, instances of MyMenu will be instantiated whenever a 'menu'
72
+ # content type is resolved.
73
+ # The mapping can be made explicit with the optional parameters. Example:
74
+ #
75
+ # class MyFoo < WCC::Contentful::Model::Foo
76
+ # register_for_content_type 'bar' # MyFoo is assumed
77
+ # end
78
+ #
79
+ # # in initializers/wcc_contentful.rb
80
+ # WCC::Contentful::Model.register_for_content_type('bar', klass: MyFoo)
81
+ def self.register_for_content_type(content_type = nil, klass: nil)
82
+ klass ||= self
83
+ raise ArgumentError, "#{klass} must be a class constant!" unless klass.respond_to?(:new)
84
+ content_type ||= content_type_from_constant(klass)
85
+
86
+ @@registry[content_type] = klass
87
+ end
88
+
89
+ ##
90
+ # Returns the current registry of content type names to constants.
91
+ def self.registry
92
+ return {} unless @@registry
93
+ @@registry.dup.freeze
94
+ end
95
+
96
+ ##
97
+ # Checks if a content type has already been registered to a class and returns
98
+ # that class. If nil, the generated WCC::Contentful::Model::{content_type} class
99
+ # will be resolved for this content type.
100
+ def self.registered?(content_type)
101
+ @@registry[content_type]
102
+ end
24
103
  end
104
+
105
+ # rubocop:enable Style/ClassVars
@@ -40,9 +40,16 @@ module WCC::Contentful
40
40
  new(raw, context) if raw.present?
41
41
  end
42
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) }
43
+ define_singleton_method(:find_all) do |filter = nil, context = nil|
44
+ if filter
45
+ filter.transform_keys! { |k| k.to_s.camelize(:lower) }
46
+ bad_fields = filter.keys.reject { |k| fields.include?(k) }
47
+ raise ArgumentError, "These fields do not exist: #{bad_fields}" unless bad_fields.empty?
48
+ end
49
+
50
+ query = WCC::Contentful::Model.store.find_all(content_type: content_type)
51
+ query = query.apply(filter) if filter
52
+ query.map { |r| new(r, context) }
46
53
  end
47
54
 
48
55
  define_singleton_method(:find_by) do |filter, context = nil|
@@ -50,11 +57,14 @@ module WCC::Contentful
50
57
  bad_fields = filter.keys.reject { |k| fields.include?(k) }
51
58
  raise ArgumentError, "These fields do not exist: #{bad_fields}" unless bad_fields.empty?
52
59
 
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) }
60
+ result = WCC::Contentful::Model.store.find_by(content_type: content_type, filter: filter)
61
+ new(result, context)
62
+ end
63
+
64
+ define_singleton_method(:inherited) do |subclass|
65
+ # only register if it's not already registered
66
+ return if WCC::Contentful::Model.registered?(typedef.content_type)
67
+ WCC::Contentful::Model.register_for_content_type(typedef.content_type, klass: subclass)
58
68
  end
59
69
 
60
70
  define_method(:initialize) do |raw, context = nil|
@@ -6,43 +6,94 @@ require_relative 'model_validators/dsl'
6
6
 
7
7
  module WCC::Contentful::ModelValidators
8
8
  def schema
9
- return if @field_validations.nil? || @field_validations.empty?
10
- field_validations = @field_validations
9
+ return if validations.nil? || validations.empty?
11
10
 
12
- # "page": {
13
- # "sys": { ... }
14
- # "fields": {
15
- # "title": { ... },
16
- # "sections": { ... },
17
- # ...
18
- # }
19
- # }
11
+ all_field_validations =
12
+ validations.each_with_object({}) do |(content_type, procs), h|
13
+ next if procs.empty?
20
14
 
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) }
15
+ # "page": {
16
+ # "sys": { ... }
17
+ # "fields": {
18
+ # "title": { ... },
19
+ # "sections": { ... },
20
+ # ...
21
+ # }
22
+ # }
23
+ h[content_type] =
24
+ Dry::Validation.Schema do
25
+ # Had to dig through the internals of Dry::Validation to find
26
+ # this magic incantation
27
+ procs.each { |dsl| instance_eval(&dsl.to_proc) }
28
+ end
26
29
  end
27
30
 
28
31
  Dry::Validation.Schema do
29
- required('fields').schema(fields_schema)
32
+ all_field_validations.each do |content_type, fields_schema|
33
+ required(content_type).schema do
34
+ required('fields').schema(fields_schema)
35
+ end
36
+ end
30
37
  end
31
38
  end
32
39
 
40
+ def validations
41
+ # This needs to be a class variable so that subclasses defined in application
42
+ # code can add to the total package of model validations
43
+ # rubocop:disable Style/ClassVars
44
+ @@validations ||= {}
45
+ # rubocop:enable Style/ClassVars
46
+ end
47
+
48
+ ##
49
+ # Accepts a block which uses the {dry-validation DSL}[http://dry-rb.org/gems/dry-validation/]
50
+ # to validate the 'fields' object of a content type.
33
51
  def validate_fields(&block)
34
52
  raise ArgumentError, 'validate_fields requires a block' unless block_given?
35
53
  dsl = ProcDsl.new(Proc.new(&block))
36
54
 
37
- (@field_validations ||= []) << dsl
55
+ ct = try(:content_type) || name.demodulize.camelize(:lower)
56
+ (validations[ct] ||= []) << dsl
38
57
  end
39
58
 
59
+ ##
60
+ # Validates a single field is of the expected type.
61
+ # Type expectations are one of:
62
+ #
63
+ # [:String] the field type must be `Symbol` or `Text`
64
+ # [:Int] the field type must be `Integer`
65
+ # [:Float] the field type must be `Number`
66
+ # [:DateTime] the field type must be 'Date'
67
+ # [:Asset] the field must be a link and the `linkType` must be `Asset`
68
+ # [:Link] the field must be a link and the `linkType` must be `Entry`.
69
+ # [:Location] the field type must be `Location`
70
+ # [:Boolean] the field type must be `Boolean`
71
+ # [:Json] the field type must be `Json` - a json blob.
72
+ # [:Array] the field must be a List.
73
+ #
74
+ # Additional validation options can be enforced:
75
+ #
76
+ # [:required] the 'Required Field' checkbox must be checked
77
+ # [:optional] the 'Required Field' checkbox must not be checked
78
+ # [:link_to] (only `:Link` or `:Array` type) the given content type(s) must be
79
+ # checked in the 'Accept only specified entry type' validations
80
+ # Example:
81
+ # validate_field :button, :Link, link_to: ['button', 'altButton']
82
+ #
83
+ # [:items] (only `:Array` type) the items of the list must be of the given type.
84
+ # Example:
85
+ # validate_field :my_strings, :Array, items: :String
86
+ #
87
+ # Examples:
88
+ # see WCC::Contentful::Model::Menu and WCC::Contentful::Model::MenuButton
40
89
  def validate_field(field, type, *options)
41
90
  dsl = FieldDsl.new(field, type, options)
42
91
 
43
- (@field_validations ||= []) << dsl
92
+ ct = try(:content_type) || name.demodulize.camelize(:lower)
93
+ (validations[ct] ||= []) << dsl
44
94
  end
45
95
 
96
+ ##
46
97
  # Accepts a content types response from the API and transforms it
47
98
  # to be acceptible for the validator.
48
99
  def self.transform_content_types_for_validation(content_types)
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ gem 'http'
4
+ require 'http'
5
+
6
+ class HttpAdapter
7
+ def call(url, query, headers = {}, proxy = {})
8
+ if proxy[:host]
9
+ HTTP[headers].via(proxy[:host], proxy[:port], proxy[:username], proxy[:password])
10
+ .get(url, params: query)
11
+ else
12
+ HTTP[headers].get(url, params: query)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ gem 'typhoeus'
4
+ require 'typhoeus'
5
+
6
+ class TyphoeusAdapter
7
+ def call(url, query, headers = {}, proxy = {})
8
+ raise NotImplementedError, 'Proxying Not Yet Implemented' if proxy[:host]
9
+
10
+ TyphoeusAdapter::Response.new(
11
+ Typhoeus.get(
12
+ url,
13
+ params: query,
14
+ headers: headers
15
+ )
16
+ )
17
+ end
18
+
19
+ Response =
20
+ Struct.new(:raw) do
21
+ delegate :body, to: :raw
22
+ delegate :to_s, to: :body
23
+ delegate :code, to: :raw
24
+ delegate :headers, to: :raw
25
+
26
+ def status
27
+ raw.code
28
+ end
29
+ end
30
+ end