fragmentary 0.1.0 → 0.3

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.
data/fragmentary.gemspec CHANGED
@@ -22,9 +22,11 @@ Gem::Specification.new do |spec|
22
22
  spec.bindir = "exe"
23
23
  spec.require_paths = ["lib"]
24
24
 
25
- spec.add_runtime_dependency "rails", ">= 4.0.0", "< 5"
25
+ spec.add_runtime_dependency "rails", "~> 5.0"
26
26
  spec.add_runtime_dependency "delayed_job_active_record", "~> 4.1"
27
27
  spec.add_runtime_dependency "wisper-activerecord", "~> 1.0"
28
+ spec.add_runtime_dependency "http", "~> 3.0.0"
29
+ spec.add_runtime_dependency "nokogiri"
28
30
  spec.add_development_dependency "bundler", "~> 1.17"
29
31
  spec.add_development_dependency "rake", "~> 10.0"
30
32
  spec.add_development_dependency "rspec", "~> 3.0"
@@ -0,0 +1,65 @@
1
+ module Fragmentary
2
+
3
+ class Config
4
+ include Singleton
5
+ attr_accessor :current_user_method, :get_sign_in_path, :post_sign_in_path, :sign_out_path,
6
+ :users, :default_user_type_mapping, :session_users, :application_root_url_column, :remote_urls
7
+
8
+ def initialize
9
+ # default
10
+ @current_user_method = :current_user
11
+ @application_root_url_column = :application_root_url
12
+ @remote_urls = []
13
+ end
14
+
15
+ def session_users=(session_users)
16
+ raise "config.session_users must be a Hash" unless session_users.is_a?(Hash)
17
+ Fragmentary.parse_session_users(session_users)
18
+ @session_users = session_users
19
+ end
20
+
21
+ def application_root_url_column=(column_name)
22
+ @application_root_url_column = column_name.to_sym
23
+ end
24
+ end
25
+
26
+ def self.current_user_method
27
+ self.config.current_user_method
28
+ end
29
+
30
+ # Parse a set of session_user options, creating session_users where needed, and return a set of user_type keys.
31
+ # session_users may take several forms:
32
+ # (1) a hash whose keys are user_type strings and whose values have the form {:credentials => credentials},
33
+ # where 'credentials' is either a hash of parameters to be submitted when logging in or a proc that
34
+ # returns those parameters.
35
+ # (2) an array of hashes as described in (1) above.
36
+ # (3) an array of user_type strings corresponding to SessionUser objects already defined.
37
+ # (4) an array containing a mixture of user_type strings and hashes as described in (1) above.
38
+ # Non-hash elements that don't represent existing SessionUser objects should raise an exception. Array
39
+ # elements that are hashes should be parsed to create new SessionUser objects. Raise an exception on
40
+ # any attempt to redefine an existing user_type.
41
+ def self.parse_session_users(session_users = nil)
42
+ return nil unless session_users
43
+ if session_users.is_a?(Array)
44
+ # Fun fact: can't use 'each_with_object' here because 'acc += parse_session_users(v)' would assign
45
+ # a different object to 'acc' on each iteration, while 'each_with_object' passes the *same* object
46
+ # to the block on each iteration.
47
+ session_users.inject([]) do |acc, v|
48
+ if v.is_a?(Hash)
49
+ acc + parse_session_users(v)
50
+ else
51
+ # v is a user_type, e.g. :admin
52
+ raise "No SessionUser exists for user_type '#{v}'" unless SessionUser.fetch(v)
53
+ acc << v
54
+ end
55
+ end
56
+ elsif session_users.is_a?(Hash)
57
+ session_users.each_with_object([]) do |(k,v), acc|
58
+ # k is the user_type, v is an options hash that typically looks like {:credentials => login_credentials} where
59
+ # login_credentials is either a hash of parameters to be submitted at login or a proc that returns those parameters.
60
+ # In the latter case, the proc is executed when we actually log in to create a new session for the specified user.
61
+ acc << k if user = SessionUser.new(k,v)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -24,13 +24,12 @@ module Fragmentary
24
24
  # redundant duplicate request.
25
25
  after_commit :touch_parent, :on => [:update, :destroy]
26
26
 
27
- attr_accessible :parent_id, :root_id, :record_id, :user_id, :user_type, :key
28
-
29
27
  attr_accessor :indexed_children
30
28
 
31
- validate :root_id, :presence => true
32
-
33
- cache_timestamp_format = :usec # Not be needed for Rails 5, which uses :usec by default.
29
+ # Set cache timestamp format to :usec instead of :nsec because the latter is greater precision than Postgres supports,
30
+ # resulting in mismatches between timestamps on a newly created fragment and one retrieved from the database.
31
+ # Probably not needed for Rails 5, which uses :usec by default.
32
+ self.cache_timestamp_format = :usec
34
33
 
35
34
  end
36
35
 
@@ -49,14 +48,14 @@ module Fragmentary
49
48
  module ClassMethods
50
49
 
51
50
  def root(options)
52
- if options[:type].constantize.requestable?
51
+ if fragment = options[:fragment]
52
+ raise ArgumentError, "You passed Fragment #{fragment.id} to Fragment.root, but it's a child of Fragment #{fragment.parent_id}" if fragment.parent_id
53
+ else
53
54
  klass, search_attributes, options = base_class.attributes(options)
54
55
  fragment = klass.where(search_attributes).includes(:children).first_or_initialize(options); fragment.save if fragment.new_record?
55
56
  fragment.set_indexed_children if fragment.child_search_key
56
- fragment
57
- else
58
- raise RangeError, "#{options[:type]} is not a root fragment class"
59
57
  end
58
+ fragment
60
59
  end
61
60
 
62
61
  # Each fragment record is unique by type and parent_id (which is nil for a root_fragment) and for some types also by
@@ -72,8 +71,14 @@ module Fragmentary
72
71
  # Collect the attributes to be used when searching for an existing fragment. Fragments are unique by these values.
73
72
  search_attributes = {}
74
73
 
75
- parent_id = options.delete(:parent_id)
76
- search_attributes.merge!(:parent_id => parent_id) if parent_id
74
+ if (parent_id = options.delete(:parent_id))
75
+ search_attributes.merge!(:parent_id => parent_id)
76
+ else
77
+ application_root_url_column = Fragmentary.config.application_root_url_column
78
+ if (application_root_url = options.delete(application_root_url_column)) && column_names.include?(application_root_url_column.to_s)
79
+ search_attributes.merge!(application_root_url_column => application_root_url)
80
+ end
81
+ end
77
82
 
78
83
  [:record_id, :user_id, :user_type, :key].each do |attribute_name|
79
84
  if klass.needs?(attribute_name)
@@ -94,16 +99,21 @@ module Fragmentary
94
99
  @@cache_store ||= Rails.application.config.action_controller.cache_store
95
100
  end
96
101
 
102
+ # ToDo: combine this with Fragment.root
97
103
  def existing(options)
98
- options.merge!(:type => name) unless self == base_class
99
- raise ArgumentError, "A 'type' attribute is needed in order to retrieve a fragment" unless options[:type]
100
- klass, search_attributes, options = base_class.attributes(options)
101
- # We merge options because it may include :record_id, which may be needed for uniqueness even
102
- # for classes that don't 'need_record_id' if the parent_id isn't available.
103
- fragment = klass.where(search_attributes.merge(options)).includes(:children).first
104
- # Unlike Fragment.root and Fragment#child we don't instantiate a record if none is found,
105
- # so fragment may be nil.
106
- fragment.try :set_indexed_children if fragment.try :child_search_key
104
+ if fragment = options[:fragment]
105
+ raise ArgumentError, "You passed Fragment #{fragment.id} to Fragment.existing, but it's a child of Fragment #{fragment.parent_id}" if fragment.parent_id
106
+ else
107
+ options.merge!(:type => name) unless self == base_class
108
+ raise ArgumentError, "A 'type' attribute is needed in order to retrieve a fragment" unless options[:type]
109
+ klass, search_attributes, options = base_class.attributes(options)
110
+ # We merge options because it may include :record_id, which may be needed for uniqueness even
111
+ # for classes that don't 'need_record_id' if the parent_id isn't available.
112
+ fragment = klass.where(search_attributes.merge(options)).includes(:children).first
113
+ # Unlike Fragment.root and Fragment#child we don't instantiate a record if none is found,
114
+ # so fragment may be nil.
115
+ fragment.try :set_indexed_children if fragment.try :child_search_key
116
+ end
107
117
  fragment
108
118
  end
109
119
 
@@ -111,20 +121,53 @@ module Fragmentary
111
121
  self
112
122
  end
113
123
 
124
+ # There is one queue per user_type per application instance (the current app and any external instances). The queues
125
+ # for all fragments are held in common by the Fragment base class here in @@request_queues but are also indexed on a
126
+ # subclass basis by an individual subclass's user_types (see the inherited hook below). As well as being accessible
127
+ # here as Fragment.request_queues, the queues are also available without indexation as RequestQueue.all.
114
128
  def request_queues
115
- @@request_queues ||= Hash.new do |hash, user_type|
116
- hash[user_type] = RequestQueue.new(user_type)
129
+ @@request_queues ||= Hash.new do |hsh, host_url|
130
+ # As well as acting as a hash key to index the set of request queues for a given target application instance
131
+ # (for which its uniqueness is the only requirement), host_url is also passed to the RequestQueue constructor,
132
+ # from which it is used:
133
+ # (i) by the RequestQueue::Sender to derive the name of the delayed_job queue that will be used to process the
134
+ # queued requests if the sender is invoked in asynchronous mode - see RequestQueue::Sender#schedulerequests.
135
+ # (ii) by the Fragmentary::InternalUserSession instantiated by the Sender to configure the session_host.
136
+ hsh[host_url] = Hash.new do |hsh2, user_type|
137
+ hsh2[user_type] = RequestQueue.new(user_type, host_url)
138
+ end
117
139
  end
118
- if self == base_class
119
- @@request_queues
120
- else
121
- return nil unless new.requestable?
122
- user_types.each_with_object({}){|user_type, queues| queues[user_type] = @@request_queues[user_type]}
140
+ end
141
+
142
+ # Subclass-specific request_queues
143
+ def inherited(subclass)
144
+ subclass.instance_eval do
145
+
146
+ def request_queues
147
+ super # ensure that @@request_queues has been defined
148
+ @request_queues ||= begin
149
+ app_root_url = Rails.application.routes.url_helpers.root_url
150
+ remote_urls = Fragmentary.config.remote_urls
151
+ user_types.each_with_object( Hash.new {|hsh0, url| hsh0[url] = {}} ) do |user_type, hsh|
152
+ # Internal request queues
153
+ hsh[app_root_url][user_type] = @@request_queues[app_root_url][user_type]
154
+ # External request queues
155
+ if remote_urls.any?
156
+ unless Rails.application.routes.default_url_options[:host]
157
+ raise "Can't create external request queues without setting Rails.application.routes.default_url_options[:host]"
158
+ end
159
+ remote_urls.each {|remote_url| hsh[remote_url][user_type] = @@request_queues[remote_url][user_type]}
160
+ end
161
+ end
162
+ end
163
+ end
164
+
123
165
  end
166
+ super
124
167
  end
125
168
 
126
- def remove_queued_request(user:, record_id:)
127
- request_queues[user_type(user)].remove_path(request_path(record_id))
169
+ def remove_queued_request(user:, request_path:)
170
+ request_queues.each{|key, hsh| hsh[user_type(user)].remove_path(request_path)}
128
171
  end
129
172
 
130
173
  def subscriber
@@ -141,7 +184,7 @@ module Fragmentary
141
184
  # create or retrieve a Fragment of that class. A user_id is needed for example when caching user-specific content
142
185
  # such as a user profile. When the fragment is instantiated using FragmentsHelper methods 'cache_fragment' or
143
186
  # 'CacheBuilder.cache_child', a :user option is added to the options hash automatically from the value of 'current_user'.
144
- # The user_id is extracted from this option in Fragment.find_or_create.
187
+ # The user_id is extracted from this option in Fragment.attributes.
145
188
  def needs_user_id
146
189
  self.extend NeedsUserId
147
190
  end
@@ -151,9 +194,24 @@ module Fragmentary
151
194
  # differently depending on the type of user, e.g. to distinguish between content seen by signed in users and those not
152
195
  # signed in. When the fragment is instantiated using FragmentsHelper methods 'cache_fragment' or 'CacheBuilder.cache_child',
153
196
  # a :user option is added to the options hash automatically from the value of 'current_user'. The user_type is extracted
154
- # from this option in Fragment.find_or_create.
155
- def needs_user_type
197
+ # from this option in Fragment.attributes.
198
+ #
199
+ # For each class that declares 'needs_user_type', a set of user_types is defined that determines the set of request_queues
200
+ # that will be used to send requests to the application when a fragment is touched. By default these user_types are defined
201
+ # globally using 'Fragmentary.setup' but they can alternatively be set on a class-specific basis by passing a :session_users
202
+ # option to 'needs_user_type'. See 'Fragmentary.parse_session_users' for details.
203
+ def needs_user_type(options = {})
156
204
  self.extend NeedsUserType
205
+ instance_eval do
206
+ @user_type_mapping = options[:user_type_mapping]
207
+ def self.user_type(user)
208
+ (@user_type_mapping || Fragmentary.config.default_user_type_mapping).try(:call, user)
209
+ end
210
+ @user_types = Fragmentary.parse_session_users(options[:session_users] || options[:types] || options[:user_types])
211
+ def self.user_types
212
+ @user_types || Fragmentary.config.session_users.keys
213
+ end
214
+ end
157
215
  end
158
216
 
159
217
  def needs_key(options = {})
@@ -202,17 +260,25 @@ module Fragmentary
202
260
  # by the fragment.
203
261
  class << record_type_subscription
204
262
  set_callback :after_destroy, :after, ->{subscriber.client.remove_fragments_for_record(record.id)}
263
+ set_callback :after_create, :after, ->{subscriber.client.try_request_for_record(record.id)}
205
264
  end
206
265
  end
207
266
 
208
- def record
209
- record_type.constantize.find(record_id)
210
- end
267
+ self.extend RecordClassMethods
268
+ define_method(:record){record_type.constantize.find(record_id)}
211
269
  end
212
270
  end
213
271
 
214
- def remove_fragments_for_record(record_id)
215
- where(:record_id => record_id).each(&:destroy)
272
+ module RecordClassMethods
273
+ def remove_fragments_for_record(record_id)
274
+ where(:record_id => record_id).each(&:destroy)
275
+ end
276
+
277
+ def try_request_for_record(record_id)
278
+ if requestable?
279
+ queue_request(request(record_id))
280
+ end
281
+ end
216
282
  end
217
283
 
218
284
  def needs_record_id?
@@ -245,7 +311,7 @@ module Fragmentary
245
311
  # hasn't yet been requested, e.g. an assumption created on an article page won't necessarily have been rendered on the
246
312
  # opinion analysis page.
247
313
  def touch_fragments_for_record(record_id)
248
- fragments_for_record(record_id).each(&:touch)
314
+ fragments_for_record(record_id).includes({:parent => :parent}).each(&:touch)
249
315
  end
250
316
 
251
317
  def fragments_for_record(record_id)
@@ -260,24 +326,12 @@ module Fragmentary
260
326
  nil
261
327
  end
262
328
 
263
- def queue_request(*args)
264
- puts " queue request for #{self.name}: #{args.inspect}"
265
- if r = request(*args)
266
- request_queues.each{|key, queue| queue << r}
267
- end
329
+ def queue_request(request=nil)
330
+ request_queues.each{|key, hsh| hsh.each{|key2, queue| queue << request}} if request
268
331
  end
269
332
 
270
333
  def requestable?
271
- respond_to? :request_path
272
- end
273
-
274
- # Subclasses that define a class method self.request_path also need to override this method
275
- def request(*args)
276
- if respond_to? :request_path
277
- raise "You can't call Fragment.request for a subclass that defines 'request_path'. #{name} needs its own request implementation."
278
- else
279
- raise "There is no 'request' class method defined for the #{name} class."
280
- end
334
+ respond_to?(:request_path)
281
335
  end
282
336
 
283
337
  # The instance method 'request_method' is defined in terms of this.
@@ -291,7 +345,11 @@ module Fragmentary
291
345
 
292
346
  # The instance method 'request_options' is defined in terms of this.
293
347
  def request_options
294
- nil
348
+ {}
349
+ end
350
+
351
+ def request
352
+ raise NotImplementedError
295
353
  end
296
354
 
297
355
  # This method defines the handler for the creation of new list items. The method takes:
@@ -382,7 +440,10 @@ module Fragmentary
382
440
  # rendered on its own, e.g. inserted by ajax into a parent that is already on the page. In this case the
383
441
  # children won't have already been loaded or indexed.
384
442
  def child(options)
385
- begin
443
+ if child = options[:child]
444
+ raise ArgumentError, "You passed a child fragment to a parent it's not a child of." unless child.parent_id == self.id
445
+ child
446
+ else
386
447
  existing = options.delete(:existing)
387
448
  # root_id and parent_id are passed from parent to child. For all except root fragments, root_id is stored explicitly.
388
449
  derived_options = {:root_id => root_id || id}
@@ -412,10 +473,7 @@ module Fragmentary
412
473
  fragment_children = fragment.children
413
474
  fragment.set_indexed_children if fragment.child_search_key
414
475
  end
415
-
416
476
  fragment
417
- rescue => e
418
- Rails.logger.error e.message + "\n " + e.backtrace.join("\n ")
419
477
  end
420
478
  end
421
479
 
@@ -436,7 +494,7 @@ module Fragmentary
436
494
  end
437
495
 
438
496
  def touch(*args, no_request: false)
439
- request_queues.each{|key, queue| queue << request} if request && !no_request
497
+ request_queues.each{|key, hsh| hsh.each{|key2, queue| queue << request}} if request && !no_request
440
498
  super(*args)
441
499
  end
442
500
 
@@ -459,10 +517,13 @@ module Fragmentary
459
517
  touch(:no_request => no_request) unless children.any?
460
518
  end
461
519
 
520
+ # Touch this fragment and all descendants that have entries in the cache. Destroy any that
521
+ # don't have cache entries.
462
522
  def touch_or_destroy
463
523
  puts " touch_or_destroy #{self.class.name} #{id}"
464
524
  if cache_exist?
465
525
  children.each(&:touch_or_destroy)
526
+ # if there are children, this will be touched automatically once they are.
466
527
  touch(:no_request => true) unless children.any?
467
528
  else
468
529
  destroy # will also destroy all children because of :dependent => :destroy
@@ -529,14 +590,6 @@ module Fragmentary
529
590
  def needs_user_type?
530
591
  true
531
592
  end
532
-
533
- def user_types
534
- ['admin', 'signed_in']
535
- end
536
-
537
- def user_type(user)
538
- user ? (user.is_an_admin? ? "admin" : "signed_in") : "signed_out"
539
- end
540
593
  end
541
594
 
542
595
  module NeedsKey
@@ -2,61 +2,76 @@ module Fragmentary
2
2
 
3
3
  module FragmentsHelper
4
4
 
5
- def cache_fragment(options)
6
- no_cache = options.delete(:no_cache)
7
- options.reverse_merge!(:user => current_user) if respond_to?(:current_user)
8
- fragment = options.delete(:fragment) || Fragmentary::Fragment.base_class.root(options)
9
- builder = CacheBuilder.new(fragment, template = self)
10
- unless no_cache
11
- cache fragment, :skip_digest => true do
12
- yield(builder)
13
- end
14
- else
15
- yield(builder)
16
- end
17
- self.output_buffer = WidgetParser.new(self).parse_buffer
5
+ def cache_fragment(options, &block)
6
+ options.reverse_merge!(Fragmentary.config.application_root_url_column => self.root_url.gsub(%r{https?://}, ''))
7
+ CacheBuilder.new(self).cache_fragment(options, &block)
18
8
  end
19
9
 
20
10
  def fragment_builder(options)
21
- template = options.delete(:template)
22
- options.reverse_merge!(:user => current_user) if respond_to?(:current_user)
23
- CacheBuilder.new(Fragmentary::Fragment.base_class.existing(options), template)
11
+ # the template option is deprecated but avoids breaking prior usage
12
+ template = options.delete(:template) || self
13
+ options.reverse_merge!(:user => Template.new(template).current_user)
14
+ options.reverse_merge!(Fragmentary.config.application_root_url_column => self.root_url.gsub(%r{https?://}, ''))
15
+ CacheBuilder.new(template, Fragmentary::Fragment.base_class.existing(options))
24
16
  end
25
17
 
26
-
27
18
  class CacheBuilder
28
19
  include ::ActionView::Helpers::CacheHelper
29
20
  include ::ActionView::Helpers::TextHelper
30
21
 
31
- attr_accessor :fragment, :template
22
+ attr_reader :fragment
32
23
 
33
- def initialize(fragment, template)
24
+ def initialize(template, fragment = nil)
34
25
  @fragment = fragment
35
26
  @template = template
36
27
  end
37
28
 
38
- def cache_child(options)
29
+ def cache_fragment(options, &block)
39
30
  no_cache = options.delete(:no_cache)
40
31
  insert_widgets = options.delete(:insert_widgets)
41
- options.reverse_merge!(:user => template.current_user) if template.respond_to?(:current_user)
42
- child = options.delete(:child) || fragment.child(options)
43
- builder = CacheBuilder.new(child, template)
32
+ options.reverse_merge!(:user => Template.new(@template).current_user)
33
+ # If the CacheBuilder was instantiated with an existing fragment, next_fragment is its child;
34
+ # otherwise it is the root fragment specified by the options provided.
35
+ next_fragment = @fragment.try(:child, options) || Fragmentary::Fragment.base_class.root(options)
36
+ builder = CacheBuilder.new(@template, next_fragment)
44
37
  unless no_cache
45
- template.cache child, :skip_digest => true do
38
+ @template.cache next_fragment, :skip_digest => true do
46
39
  yield(builder)
47
40
  end
48
41
  else
49
42
  yield(builder)
50
43
  end
51
- template.output_buffer = WidgetParser.new(template).parse_buffer if insert_widgets
44
+ @template.output_buffer = WidgetParser.new(@template).parse_buffer if (!@fragment || insert_widgets)
52
45
  end
53
46
 
47
+ alias cache_child cache_fragment
48
+
49
+ private
50
+
54
51
  def method_missing(method, *args)
55
- fragment.send(method, *args)
52
+ @fragment.send(method, *args)
56
53
  end
57
54
 
58
55
  end
59
56
 
60
57
  end
61
58
 
59
+
60
+ # Just a wrapper to allow us to call a configurable current_user_method on the template
61
+ class Template
62
+
63
+ def initialize(template)
64
+ @template = template
65
+ end
66
+
67
+ def current_user
68
+ return nil unless methd = Fragmentary.current_user_method
69
+ if @template.respond_to? methd
70
+ @template.send methd
71
+ else
72
+ raise NoMethodError, "The current_user_method '#{methd.to_s}' specified doesn't exist"
73
+ end
74
+ end
75
+ end
76
+
62
77
  end
@@ -33,7 +33,9 @@ module Fragmentary
33
33
  end
34
34
 
35
35
  def after_update_broadcast
36
- broadcast(:after_update, self)
36
+ Rails.logger.info "\n***** #{start = Time.now} broadcasting :after_update from #{self.class.name} #{self.id}\n"
37
+ broadcast(:after_update, self) if self.previous_changes.any?
38
+ Rails.logger.info "\n***** #{Time.now} broadcast :after_update from #{self.class.name} #{self.id} took #{(Time.now - start) * 1000} ms\n"
37
39
  end
38
40
 
39
41
  def after_destroy_broadcast
@@ -3,28 +3,13 @@ module Fragmentary
3
3
  class Request
4
4
  attr_reader :method, :path, :options, :parameters
5
5
 
6
- def initialize(method, path, parameters=nil, options=nil)
6
+ def initialize(method, path, parameters=nil, options={})
7
7
  @method, @path, @parameters, @options = method, path, parameters, options
8
8
  end
9
9
 
10
10
  def ==(other)
11
11
  method == other.method and path == other.path and parameters == other.parameters and options == other.options
12
12
  end
13
-
14
- def to_proc
15
- method = @method; path = @path; parameters = @parameters; options = @options.try :dup
16
- if @options.try(:[], :xhr)
17
- Proc.new do
18
- puts " * Sending xhr request '#{method.to_s} #{path}'" + (!parameters.nil? ? " with #{parameters.inspect}" : "")
19
- send(:xhr, method, path, parameters, options)
20
- end
21
- else
22
- Proc.new do
23
- puts " * Sending request '#{method.to_s} #{path}'" + (!parameters.nil? ? " with #{parameters.inspect}" : "")
24
- send(method, path, parameters, options)
25
- end
26
- end
27
- end
28
13
  end
29
14
 
30
15
  end