mattly-exegesis 0.2.0 → 0.2.1

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/README.rdoc CHANGED
@@ -9,15 +9,9 @@ A CouchDB ODM (Object/Document Mapper) in Ruby.
9
9
 
10
10
  == Features:
11
11
 
12
- Encourages per-"Account" databases. Actually, does not even currently provide a way to do a
13
- "singleton" or global database, however this is planned. Since a given class (say, "Article")
14
- cannot know what database it is supposed to get/search from you cannot do classical class-based
15
- finders such as "Article.find('value')".
12
+ Encourages per-"Account" databases. Actually, does not even currently provide a way to do a "singleton" or global database, however this is planned. Since a given class (say, "Article") cannot know what database it is supposed to get/search from you cannot do classical class-based finders such as "Article.find('value')". While it might be possible to pass in a database to use for some class-wide view, Exegesis takes the opinion that this is bad design for couchdb for the reasons that views may return multiple document types other than the desired class, and that views should be scoped to objects that mixin the database accessors.
16
13
 
17
- CouchDB is table-less, and Exegesis's design reflects this. In CouchDB, Documents are retrieved
18
- by their unique id, or can be queried from a view function in a design document. Exegesis provides
19
- tools to aid this. Additionally, since view functions can be used for map/reduce computations against
20
- your documents, Exegesis helps you get non-document data out of your views.
14
+ CouchDB is table-less, and Exegesis's design reflects this. In CouchDB, Documents are retrieved by their unique id, or can be queried from a view function in a design document. Exegesis provides tools to aid this. Additionally, since view functions can be used for map/reduce computations against your documents, Exegesis helps you get non-document data out of your views.
21
15
 
22
16
  == Examples:
23
17
 
@@ -64,4 +58,6 @@ For running the tests:
64
58
  * Test::Unit (you got it)
65
59
  * Context (http://github.com/jeremymcanally/context, can install from github gems)
66
60
  * Matchy (http://github.com/jeremymcanally/matchy, github gem version out of date; clone, build & install for now)
67
- * Zebra (http://github.com/giraffesoft/zerba, depends on jeremymcanally-matchy, which is out of date; clone, build & install for now)
61
+ * Zebra (http://github.com/giraffesoft/zerba, depends on jeremymcanally-matchy, which is out of date; clone, build & install for now)
62
+
63
+ The test suite creates and destroys a database for each test that requires access to the database. This is slow, and the test suite may take some time to run. However, I would rather the test suite be slow and accurate than quick and full of mocking.
data/Rakefile ADDED
@@ -0,0 +1,32 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ begin
6
+ require 'jeweler'
7
+ Jeweler::Tasks.new do |s|
8
+ s.name = "exegesis"
9
+ s.summary = "TODO"
10
+ s.email = "matt@flowerpowered.com"
11
+ s.homepage = "http://github.com/mattly/exegesis"
12
+ s.description = "A Document <> Object Mapper for CouchDB Documents"
13
+ s.authors = ["Matt Lyon"]
14
+ s.add_dependency('rest-client', '>= 0.12.6')
15
+ end
16
+ rescue LoadError
17
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
18
+ end
19
+
20
+ Rake::TestTask.new do |t|
21
+ t.libs << 'lib'
22
+ t.pattern = 'test/**/*_test.rb'
23
+ t.verbose = false
24
+ end
25
+
26
+ Rake::RDocTask.new do |rdoc|
27
+ rdoc.rdoc_dir = 'rdoc'
28
+ rdoc.title = 'test-gem'
29
+ rdoc.options << '--line-numbers' << '--inline-source'
30
+ rdoc.rdoc_files.include('README*')
31
+ rdoc.rdoc_files.include('lib/**/*.rb')
32
+ end
data/VERSION.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  ---
2
2
  :major: 0
3
3
  :minor: 2
4
- :patch: 0
4
+ :patch: 1
@@ -19,24 +19,35 @@ module Exegesis
19
19
  end
20
20
  end
21
21
 
22
- # A hash mapping design names to class names.
23
- def designs
24
- @designs ||= {}
25
- end
26
-
27
22
  # declare a design document for this database. Creates a new class and yields a given block to the class to
28
23
  # configure the design document and declare views; See Class methods for Exegesis::Design
29
24
  def design design_name, opts={}, &block
30
25
  klass_name = "#{design_name.to_s.capitalize}Design"
31
26
  klass = const_set(klass_name, Class.new(Exegesis::Design))
32
- designs[design_name] = klass
33
27
  klass.design_directory = opts[:directory] || self.designs_directory + design_name.to_s
34
28
  klass.design_name = opts[:name] || design_name.to_s
35
29
  klass.compose_canonical
36
30
  klass.class_eval &block
37
31
  define_method design_name do
38
- @designs ||= {}
39
- @designs[design_name] ||= klass.new(self)
32
+ @exegesis_designs ||= {}
33
+ @exegesis_designs[design_name] ||= klass.new(self)
34
+ end
35
+ end
36
+
37
+ def named_document document_name, opts={}, &block
38
+ klass_name = document_name.to_s.capitalize.gsub(/_(\w)/) { $1.capitalize }
39
+ klass = const_set(klass_name, Class.new(Exegesis::GenericDocument))
40
+ klass.unique_id { document_name.to_s }
41
+ klass.class_eval &block if block
42
+ define_method document_name do
43
+ @exegesis_named_documents ||= {}
44
+ @exegesis_named_documents[document_name] ||= begin
45
+ get(document_name.to_s)
46
+ rescue RestClient::ResourceNotFound
47
+ doc = klass.new({}, self)
48
+ doc.save
49
+ doc
50
+ end
40
51
  end
41
52
  end
42
53
  end
@@ -62,11 +73,18 @@ module Exegesis
62
73
  @server.get @uri # raise RestClient::ResourceNotFound if the database does not exist
63
74
  end
64
75
 
76
+ def to_s
77
+ "#<#{self.class.name}(Exegesis::Database):#{self.object_id} uri=#{uri}>"
78
+ end
79
+ alias :inspect :to_s
80
+
81
+ # performs a raw GET request against the database
65
82
  def raw_get id, options={}
66
83
  keys = options.delete(:keys)
84
+ id = Exegesis::Http.escape_id id
67
85
  url = Exegesis::Http.format_url "#{@uri}/#{id}", options
68
86
  if id.match(%r{^_design/.*/_view/.*$}) && keys
69
- Exegesis::Http.post url, {:keys => keys}
87
+ Exegesis::Http.post url, {:keys => keys}.to_json
70
88
  else
71
89
  Exegesis::Http.get url
72
90
  end
@@ -74,7 +92,13 @@ module Exegesis
74
92
 
75
93
  # GETs a document with the given id from the database
76
94
  def get id, opts={}
77
- Exegesis.instantiate raw_get(id), self
95
+ if id.kind_of?(Array)
96
+ collection = opts.delete(:collection) # nil or true for yes, false for no
97
+ r = post '_all_docs?include_docs=true', {"keys"=>id}
98
+ r['rows'].map {|d| Exegesis.instantiate d['doc'], self }
99
+ else
100
+ Exegesis.instantiate raw_get(id), self
101
+ end
78
102
  end
79
103
 
80
104
  # saves a document or collection thereof
@@ -92,17 +116,17 @@ module Exegesis
92
116
  end
93
117
 
94
118
  # PUTs the body to the given id in the database
95
- def put id, body
96
- Exegesis::Http.put "#{@uri}/#{id}", body
119
+ def put id, body, headers={}
120
+ Exegesis::Http.put "#{@uri}/#{id}", (body || '').to_json, headers
97
121
  end
98
122
 
99
123
  # POSTs the body to the database
100
- def post url, body={}
124
+ def post url, body={}, headers={}
101
125
  if body.is_a?(Hash) && body.empty?
102
126
  body = url
103
127
  url = ''
104
128
  end
105
- Exegesis::Http.post "#{@uri}/#{url}", body
129
+ Exegesis::Http.post "#{@uri}/#{url}", (body || '').to_json, headers
106
130
  end
107
131
  end
108
132
  end
@@ -1,6 +1,7 @@
1
1
  require 'pathname'
2
2
  module Exegesis
3
3
  class Design
4
+
4
5
  include Exegesis::Document
5
6
 
6
7
  def self.design_directory= dir
@@ -36,46 +37,67 @@ module Exegesis
36
37
  }
37
38
  end
38
39
 
40
+ def self.views
41
+ @views ||= canonical_design['views'].keys
42
+ end
43
+
44
+ def self.reduceable? view_name
45
+ view_name = view_name.to_s
46
+ views.include?(view_name) && canonical_design['views'][view_name].has_key?('reduce')
47
+ end
48
+
39
49
  def self.view name, default_options={}
40
- define_method name do |key, *opts|
41
- view name, key, opts.first, default_options
50
+ define_method name do |*opts|
51
+ options = parse_opts opts.shift, opts.first, default_options
52
+ Exegesis::DocumentCollection.new(call_view(name, options), database)
42
53
  end
43
54
  end
44
55
 
45
56
  def self.docs name, default_options={}
46
- default_options = {:include_docs => true, :reduce => false}.merge(default_options)
57
+ view_name = default_options.delete(:view) || name
58
+ raise ArgumentError, "missing view #{view_name}" unless views.include?(view_name.to_s)
59
+ if [:reduce, :group, :group_level].any? {|key| default_options.has_key?(key)}
60
+ raise ArgumentError, "cannot reduce (:group, :group_level, :reduce) on a docs view"
61
+ end
62
+
63
+ default_options = {:include_docs => true}.merge(default_options)
64
+ default_options.update({:reduce => false}) if reduceable?(view_name)
65
+
47
66
  define_method name do |*opts|
48
67
  key = opts.shift
49
68
  options = parse_opts key, opts.first, default_options
50
- response = call_view name, options
51
- ids = []
52
- response.inject([]) do |memo, doc|
53
- unless ids.include?(doc['id'])
54
- ids << doc['id']
55
- memo << Exegesis.instantiate(doc['doc'], database)
56
- end
57
- memo
58
- end
69
+ Exegesis::DocumentCollection.new(call_view(view_name, options), database)
59
70
  end
60
71
  end
61
72
 
62
73
  def self.hash name, default_options={}
63
- default_options = {:group => true}.merge(default_options)
64
74
  view_name = default_options.delete(:view) || name
75
+ raise ArgumentError, "missing view #{view_name}" unless views.include?(view_name.to_s)
76
+ raise NameError, "Cannot return a hash for views without a reduce function" unless reduceable?(view_name)
77
+ if default_options.has_key?(:group) && default_options[:group] == false
78
+ raise ArgumentError, "cannot turn off grouping for a hash view"
79
+ end
80
+
81
+ default_options = {:group => true}.merge(default_options)
82
+
65
83
  define_method name do |*opts|
66
- key = opts.shift
67
- options = parse_opts key, opts.first, default_options
68
- options.delete(:group) if options[:key]
84
+ options = parse_opts opts.shift, opts.first, default_options
85
+
86
+ if options.has_key?(:group) && options[:group] == false
87
+ raise ArgumentError, "cannot turn off grouping for a hash view"
88
+ end
89
+
90
+ if options[:key]
91
+ options.delete(:group)
92
+ options.delete(:group_level)
93
+ end
69
94
 
70
95
  response = call_view view_name, options
71
96
  if response.size == 1 && response.first['key'].nil?
72
97
  response.first['value']
73
98
  else
74
99
  response.inject({}) do |memo, row|
75
- if ! memo.has_key?(row['key'])
76
- memo[row['key']] = row['value']
77
- end
78
- memo
100
+ memo.update(row['key'] => row['value'])
79
101
  end
80
102
  end
81
103
  end
@@ -84,7 +106,8 @@ module Exegesis
84
106
 
85
107
  def initialize db
86
108
  begin
87
- super db.get("_design/#{design_name}"), db
109
+ super db.raw_get("_design/#{design_name}")
110
+ self.database = db
88
111
  rescue RestClient::ResourceNotFound
89
112
  db.put("_design/#{design_name}", self.class.canonical_design)
90
113
  retry
@@ -113,6 +136,7 @@ module Exegesis
113
136
  parse_key opts
114
137
  parse_keys opts
115
138
  parse_range opts
139
+ parse_reduce opts
116
140
  opts
117
141
  end
118
142
 
@@ -151,5 +175,17 @@ module Exegesis
151
175
  end
152
176
  end
153
177
 
178
+ def parse_reduce opts
179
+ if opts.has_key?(:group)
180
+ opts[:group_level] = opts.delete(:group) if opts[:group].is_a?(Numeric)
181
+ end
182
+ if opts.keys.any? {|key| [:group, :group_level].include?(key) }
183
+ raise ArgumentError, "cannot include_docs when reducing" if opts[:include_docs]
184
+ if opts.has_key?(:reduce) && opts[:reduce] == false
185
+ raise ArgumentError, "cannot reduce=false when either group or group_level is present"
186
+ end
187
+ end
188
+ end
189
+
154
190
  end
155
191
  end
@@ -0,0 +1,44 @@
1
+ module Exegesis
2
+ module Document
3
+ class Attachment
4
+
5
+ attr_reader :name, :metadata, :document
6
+
7
+ def initialize(name, thing, doc)
8
+ @document = doc
9
+ @metadata = thing
10
+ @name = name
11
+ end
12
+
13
+ def to_s
14
+ "#<Exegesis::Document::Attachment document=#{@document.uri} #{@metadata.inspect}>"
15
+ end
16
+ alias :inspect :to_s
17
+
18
+ def content_type
19
+ @metadata['content_type']
20
+ end
21
+
22
+ def length
23
+ @metadata['length'] || -1
24
+ end
25
+
26
+ def stub?
27
+ @metadata['stub'] || false
28
+ end
29
+
30
+ def file
31
+ RestClient.get("#{document.database.uri}/#{document.id}/#{name}")
32
+ end
33
+
34
+ def to_json
35
+ if @metadata['data']
36
+ {'content_type' => @metadata['content_type'], 'data' => @metadata['data']}.to_json
37
+ else
38
+ @metadata.to_json
39
+ end
40
+ end
41
+
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,53 @@
1
+ require 'base64'
2
+ module Exegesis
3
+ module Document
4
+ class Attachments < Hash
5
+
6
+ attr_accessor :document
7
+
8
+ def initialize doc
9
+ @document = doc
10
+ if @document['_attachments']
11
+ @document['_attachments'].each do |name,meta|
12
+ update(name => Exegesis::Document::Attachment.new(name, meta, document))
13
+ end
14
+ end
15
+ end
16
+
17
+ def to_s
18
+ "#<Exegesis::Document::Attachments document=#{@document.uri} attachments=#{keys.join(', ')}>"
19
+ end
20
+ alias :inspect :to_s
21
+
22
+ def dirty?
23
+ @dirty || false
24
+ end
25
+
26
+ def clean!
27
+ each do |name, attachment|
28
+ next if attachment.stub?
29
+ attachment.metadata['stub'] = true
30
+ attachment.metadata.delete('data')
31
+ end
32
+ @dirty = false
33
+ end
34
+
35
+ # saves the attachment to the database NOW. does not keep the attachment in memory once this is done.
36
+ def put(name, contents, type)
37
+ r = Exegesis::Http.put("#{document.uri}/#{name}?rev=#{document.rev}", contents, {:content_type => type})
38
+ if r['ok']
39
+ document['_rev'] = r['rev']
40
+ update(name => Exegesis::Document::Attachment.new(name, {'content_type' => type, 'stub' => true, 'length' => contents.length}, document))
41
+ end
42
+ end
43
+
44
+ def []= name, contents_and_type
45
+ @dirty = true
46
+ content = contents_and_type.shift
47
+ meta = {'data' => Base64.encode64(content).gsub(/\s/,''), 'content_type' => contents_and_type.first, 'length' => content.length}
48
+ update(name => Exegesis::Document::Attachment.new(name, meta, document))
49
+ end
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,78 @@
1
+ module Exegesis
2
+ class DocumentCollection
3
+ include Enumerable
4
+
5
+ attr_reader :rows, :parent, :index
6
+
7
+ def initialize docs=[], master=nil, index=0
8
+ @rows = docs
9
+ if master.is_a?(Exegesis::DocumentCollection)
10
+ @parent = master
11
+ else
12
+ @database = master
13
+ end
14
+ @index = index
15
+ end
16
+
17
+ def to_s
18
+ "#<Exegesis::DocumentCollection:#{object_id} database=#{database.uri} rows=#{size} depth=#{index}>"
19
+ end
20
+ alias :inspect :to_s
21
+
22
+ def database
23
+ @database || @parent.database
24
+ end
25
+
26
+ def size
27
+ @rows.length
28
+ end
29
+
30
+ def keys
31
+ @keys ||= rows.map {|r| r['key'] }.uniq
32
+ end
33
+
34
+ def values
35
+ @values ||= rows.map {|r| r['value'] }
36
+ end
37
+
38
+ def documents
39
+ @documents ||= load_documents
40
+ end
41
+
42
+ def [] key
43
+ @keymaps ||= {}
44
+ filtered_rows = rows.select do |row|
45
+ if row['key'].is_a?(Array)
46
+ row['key'][index] == key
47
+ else
48
+ row['key'] == key
49
+ end
50
+ end
51
+ new_index = index + 1
52
+ @keymaps[key] ||= self.class.new(filtered_rows, self, new_index)
53
+ end
54
+
55
+ def each &block
56
+ rows.each do |row|
57
+ yield row['key'], row['value'], documents[row['id']]
58
+ end
59
+ end
60
+
61
+ private
62
+ def load_documents
63
+ docmap = {}
64
+ if parent.nil?
65
+ non_doc_rows = rows.select {|r| ! r.has_key?('doc') }
66
+ if non_doc_rows.empty?
67
+ rows.map {|r| docmap[r['id']] = Exegesis.instantiate(r['doc'], database) }
68
+ else
69
+ database.get(non_doc_rows.map{|r| r['id']}.uniq, :include_docs=>true).each {|doc| docmap[doc.id] = doc}
70
+ end
71
+ else
72
+ rows.map {|r| docmap[r['id']] = parent.documents[r['id']] }
73
+ end
74
+ docmap
75
+ end
76
+
77
+ end
78
+ end
@@ -0,0 +1,5 @@
1
+ module Exegesis
2
+ class GenericDocument
3
+ include Exegesis::Document
4
+ end
5
+ end
@@ -1,5 +1,10 @@
1
1
  module Exegesis
2
2
  module Document
3
+ autoload :Attachments, 'exegesis/document/attachments'
4
+ autoload :Attachment, 'exegesis/document/attachment'
5
+
6
+ class MissingDatabaseError < StandardError; end
7
+ class NewDocumentError < StandardError; end
3
8
 
4
9
  def self.included base
5
10
  base.send :include, Exegesis::Model
@@ -35,6 +40,20 @@ module Exegesis
35
40
  @database = db
36
41
  end
37
42
 
43
+ def uri
44
+ raise MissingDatabaseError if database.nil?
45
+ raise NewDocumentError if rev.nil? || id.nil?
46
+ "#{database.uri}/#{id}"
47
+ end
48
+
49
+ def reload
50
+ raise NewDocumentError if rev.nil? || id.nil?
51
+ raise MissingDatabaseError if database.nil?
52
+ @attachments = nil
53
+ @references = nil
54
+ @attributes = database.raw_get(id)
55
+ end
56
+
38
57
  def == other
39
58
  self.id == other.id
40
59
  end
@@ -54,19 +73,28 @@ module Exegesis
54
73
  else
55
74
  save_document
56
75
  end
76
+ @attachments.clean! if @attachments && @attachments.dirty?
57
77
  end
58
78
 
59
79
  def update_attributes attrs={}
60
- raise ArgumentError, 'must include a matching _rev attribute' unless rev == attrs.delete('_rev')
80
+ raise ArgumentError, 'must include a matching _rev attribute' unless (rev || '') == (attrs.delete('_rev') || '')
61
81
  super attrs
62
82
  save
63
83
  end
64
84
 
85
+ def attachments
86
+ @attachments ||= Exegesis::Document::Attachments.new(self)
87
+ end
88
+
89
+ def to_json
90
+ @attributes.merge({'_attachments' => @attachments}).to_json
91
+ end
92
+
65
93
  private
66
94
 
67
95
  def save_document
68
96
  raise ArgumentError, "canont save without a database" unless database
69
- database.save self.attributes
97
+ database.save self
70
98
  end
71
99
 
72
100
  def save_with_custom_unique_id
@@ -4,13 +4,13 @@ module Exegesis
4
4
  def self.included base
5
5
  base.extend ClassMethods
6
6
  base.send :include, InstanceMethods
7
- Exegesis.model_classes[base.name] = base
8
7
  base.send :attr_accessor, :attributes, :references, :parent
9
8
  end
10
9
 
11
10
  module ClassMethods
12
11
  def expose *attrs
13
12
  opts = attrs.last.is_a?(Hash) ? attrs.pop : {}
13
+ raise ArgumentError, "casted keys cannot have defined writers" if opts[:as] && opts[:writer]
14
14
  [attrs].flatten.each do |attrib|
15
15
  attrib = attrib.to_s
16
16
  if opts[:writer]
@@ -20,6 +20,7 @@ module Exegesis
20
20
  end
21
21
  if opts[:as] == :reference
22
22
  define_reference attrib
23
+ define_reference_writer attrib unless opts[:writer] == false
23
24
  elsif opts[:as]
24
25
  define_caster attrib, opts[:as]
25
26
  else
@@ -53,6 +54,22 @@ module Exegesis
53
54
  end
54
55
  end
55
56
 
57
+ def define_reference_writer attrib
58
+ define_writer(attrib) do |val|
59
+ if val.is_a?(String)
60
+ @attributes[attrib] = val
61
+ elsif val.is_a?(Exegesis::Document)
62
+ if val.rev && val.id
63
+ @attributes[attrib] = val.id
64
+ else
65
+ raise ArgumentError, "cannot reference unsaved documents"
66
+ end
67
+ else
68
+ raise ArgumentError, "was not a document or document id"
69
+ end
70
+ end
71
+ end
72
+
56
73
  def define_caster attrib, as
57
74
  define_method(attrib) do
58
75
  @attributes[attrib] = if @attributes[attrib].is_a?(Array)
@@ -109,8 +126,9 @@ module Exegesis
109
126
 
110
127
  def cast as, value
111
128
  return nil if value.nil?
129
+ return value unless [String, Hash, Fixnum, Float].include?(value.class)
112
130
  klass = if as == :given && value.is_a?(Hash)
113
- Exegesis.model_classes[value['class']]
131
+ Exegesis.constantize(value['class'])
114
132
  elsif as.is_a?(Class)
115
133
  as
116
134
  else
@@ -22,7 +22,7 @@ module Exegesis
22
22
  end
23
23
 
24
24
  def inspect
25
- "#<Exegesis::Server #{@uri}>"
25
+ "#<Exegesis::Server:#{object_id} uri=#{@uri}>"
26
26
  end
27
27
  end
28
28
  end
@@ -15,23 +15,29 @@ module Exegesis
15
15
  end
16
16
 
17
17
  def escape_id id
18
- /^_design\/(.*)/ =~ id ? "_design/#{CGI.escape($1)}" : CGI.escape(id)
18
+ if %r{^_design/(.*)/_view/(.*)} =~ id
19
+ "_design/#{CGI.escape($1)}/_view/#{CGI.escape($2)}"
20
+ elsif /^_design\/(.*)/ =~ id
21
+ "_design/#{CGI.escape($1)}"
22
+ else
23
+ CGI.escape(id)
24
+ end
19
25
  end
20
26
 
21
- def get url
22
- JSON.parse(RestClient.get(url), :max_nesting => false)
27
+ def get url, headers={}
28
+ JSON.parse(RestClient.get(url, headers), :max_nesting => false)
23
29
  end
24
30
 
25
- def post url, body=''
26
- JSON.parse(RestClient.post(url, (body || '').to_json))
31
+ def post url, body='', headers={}
32
+ JSON.parse(RestClient.post(url, body, headers))
27
33
  end
28
34
 
29
- def put url, body=''
30
- JSON.parse(RestClient.put(url, (body || '').to_json))
35
+ def put url, body='', headers={}
36
+ JSON.parse(RestClient.put(url, body, headers))
31
37
  end
32
38
 
33
- def delete url
34
- JSON.parse(RestClient.delete(url))
39
+ def delete url, headers={}
40
+ JSON.parse(RestClient.delete(url, headers))
35
41
  end
36
42
 
37
43
  end