sk-api 1.0.6 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -1,11 +1,9 @@
1
1
  = SalesKing Api
2
2
 
3
- This plugin is used internally by SalesKing to deliver the API and
4
- can be used externally as an API client. The code is used in production at
5
- https://www.SalesKing, http://texprovider.de and http://planio.de
3
+ This gem provides an api client using ActiveResource and also provides the API's JSON-Schema
4
+ In the near future this will be decoupled and you will find links to the new gems here.
6
5
 
7
- The SalesKing API client uses ActiveResource, with some fixes due to json parsing.
8
- For now it only supports JSON, XML comes later.
6
+ The SalesKing API client uses JSON and ActiveResource, with some fixes due to json parsing.
9
7
 
10
8
  To be able to use the API you must have an SalesKing account and API access.
11
9
  Than you can start writing your own middelware-stack.
@@ -17,13 +15,209 @@ and support.
17
15
 
18
16
  gem install sk-api
19
17
 
20
- == Example
18
+ Use with bundler
21
19
 
22
- client = SkApi::Resources::Client.new(:last_name=>'Eder')
23
- client.first_name = "Meister"
24
- client.save
20
+ gem "sk-api", :require => 'sk_api'
21
+
22
+ == Usage
23
+
24
+ SalesKing's api interface is RESTful(mostly) and returns & accepts JSON data.
25
+ All resources such as clients, invoices, products can be accessed via URL's
26
+ through standard HTTP methods GET, POST, PUT and DELETE.
27
+
28
+ To give it quick shot, point your browser to my-account.salesking.eu/api/clients.json
29
+ (while beeing logged in) and you will see the JSON in the response.
30
+ This is an example of a GET-request issued by your browser.
31
+
32
+ <b>Request Method overview:</b>
33
+ GET => read
34
+ POST => create new data
35
+ PUT => update data
36
+ DELETE => delete data
37
+
38
+ Want to know more about REST style webservices?
39
+ * http://en.wikipedia.org/wiki/Representational_State_Transfer
40
+ * http://www.google.com/search?q=REST+site%3Awww.infoq.com
41
+
42
+ === Authentification & Safety
43
+
44
+ Authentification is done with HTTP basic using your SalesKing login email and password.
45
+ For a production environment it is advisable to create a user, per api
46
+ client-software, and restrict his rights with our build in role-system.
47
+
48
+ === Toolz
49
+
50
+ Since browsers do not support PUT/DELETE methods you can use CURL, a linux
51
+ command-line http client, for testing. And of course any http library supporting
52
+ http-basic-auth can be used.
53
+
54
+ * JSONView FF-Plugin - view json in firefox https://addons.mozilla.org/de/firefox/addon/10869/
55
+ * JSONovich FF-Plugin - https://addons.mozilla.org/de/firefox/addon/10122/
56
+ * A java GUI REST client http://code.google.com/p/rest-client/
57
+
58
+ === GET / Show data
59
+
60
+ Get a list of resources:
61
+ GET xy.salesking.eu/api/invoices
62
+
63
+ Get a single resource
64
+ GET xy.salesking.eu/invoices/:id
65
+
66
+ Using CURL
67
+
68
+ curl -u your@login-mail.com:password \
69
+ https://demo.salesking.eu/clients.json
70
+
71
+ Returned JSON data for a listings, abbreviated:
72
+ {
73
+ "clients": [ # array of resources => clients
74
+ { "client": { # a single resource
75
+ "number": "0800013", ..
76
+ "links":{} #links to actions for the resource (comming soon)
77
+ }, ...
78
+ ],
79
+ collection": { # information about the collection
80
+ "total_pages": 1,
81
+ "total_entries": 3,
82
+ "current_page": 1,
83
+ "per_page": 30
84
+ },
85
+ "links": { # links for the collection
86
+ "prev": "/api/clients?page=0",
87
+ "next": "/api/clients?page=2",
88
+ "self": "/api/clients?page=1"
89
+ },
90
+ }
91
+
92
+ ==== Filtering
93
+
94
+ Filtering data can be achived by adding a filter parameter to the url.
95
+
96
+ All objects which can be tagged(clients, documents, products) can be filter by a tag param, taking one or multiple tags:
97
+ GET xy.salesking.eu/api/invoices?filter[tags]=Agentur
98
+ GET xy.salesking.eu/api/invoices?filter[tags]=Agentur Grafik
99
+ # find all objects without tags
100
+ GET xy.salesking.eu/api/invoices?filter[not_tagged]=1
101
+
102
+ All objects have a q-search available. The fields to be searched in can be seen inside SalesKing, behind the search field above list.
103
+ GET xy.salesking.eu/api/invoices?q=0800013
104
+ GET xy.salesking.eu/api/invoices?filter[q]=0800013
105
+
106
+ Document specific filters
107
+ # find all documents for a client
108
+ GET xy.salesking.eu/api/invoices?filter[client]=:client_id
109
+
110
+ Client filter:
111
+ # find clients organisation or lastname starting with A
112
+ GET xy.salesking.eu/api/clients?filter[letter]=A
113
+
114
+ === POST / Create data
115
+
116
+ To create a resource you can use the gem with its methods or make a POST request containing the json for a single resource.
117
+ The JSON format can be seen in each of the resources: http://github.com/salesking/sk-api/tree/master/lib/resources/
118
+
119
+ POST https://my-sub.salesking.eu/invoices.json content-type application/json
120
+
121
+ This a minimal json to create a draft invoice:
122
+
123
+ {"invoice": {
124
+ "due_days": 14,
125
+ "line_items": [
126
+ {
127
+ "quantity_unit": "Stuck",
128
+ "tax": 19,
129
+ "price_single": 0,
130
+ "position": 1,
131
+ "quantity": 21,
132
+ "description": "dep38usabq4Boxen kostenlos"
133
+ },
134
+ {
135
+ "quantity_unit": "Stuck",
136
+ "tax": 19,
137
+ "price_single": 0.042022,
138
+ "position": 2,
139
+ "quantity": 21,
140
+ "description": "dep38usabq4 Boxen"
141
+ }
142
+ ],
143
+ "title": "Dabei",
144
+ "client_id": "c5d5xIwZGr3R56abxfpGMl",
145
+ "address_field": "Wurst Bolle\nHalve-Hahn Alle 4711\n50733 Köln-Nippes"
146
+ }}
147
+
148
+ Some quick hints on document creation:
149
+
150
+ * The line items array does not need prefixes .. (sry little inconsistency here)
151
+ * Dates are formatted like <tt>yyyy-mm-dd 2010-09-08</tt>
152
+ * <tt>"price_total": 0.88, "price_tax": 0.167</tt> can be ommited are calculated by sk => read only
153
+ * <tt>due_date</tt> if left out will be auto calculated from due days. If date and due date are given due_days are not taken into consideration.
154
+ * <tt>status: draft</tt> is default, so can be omitted
155
+ * <tt>client_id</tt> must be given as uuid of the client object
156
+ * line items with quantity of 0 are kicked
157
+ * If you create the doc as draft you can leave out any dates. When “opened” in salesking the date will be set and due date is calculated from due_days
158
+ * If you create the invoice as "open" you must set a number(and date), by now there is no api method to get the next number.
159
+ * The proposed workflow is to create the document as draft and continue (open/send/close) in SalesKing.
160
+
161
+
162
+ For more information on how to use take a look into the tests/specs:
163
+
164
+ http://github.com/salesking/sk-api/tree/master/spec/resources/
165
+
166
+ === PUT / Update data
167
+
168
+ To Update a client:
169
+
170
+ PUT ( application/json ) to ../api/clients/:id
171
+ {
172
+ 'client':
173
+ {
174
+ 'gender':'male',
175
+ 'first_name': "Andrew"
176
+ }
177
+ }
178
+
179
+ === DELETE / kill data
180
+
181
+ To delete a record simply issue a DELETE request to a resource url:
182
+
183
+ DELETE https://demo.salesking.eu/clients/:id
184
+
185
+ == Ruby Example
186
+
187
+ Since the sk-api gem provides a wrapper around the whole url/request stuff its
188
+ pretty straight forward to use:
189
+
190
+ class SomeMiddleware
191
+ include SKApi::Resources
192
+
193
+ def initialize()
194
+ # init api connection => just an example depending on your code structure do it wherever you want
195
+ set_api_connection
196
+ end
197
+
198
+ def create_client(last_name)
199
+ # create a new client object
200
+ client = SKApi::Resources::Client.new(:last_name=> last_name)
201
+ # set more data
202
+ client.first_name = "Meister"
203
+ # save it, which also does the remote saving action
204
+ client.save
205
+ end
206
+
207
+ def set_api_connection
208
+ SKApi::Resources::Base.set_connection({
209
+ :site => 'my_sub.salesking.eu',
210
+ :user => 'demo@salesking.eu',
211
+ :password => 'password',
212
+ :format => :json # symbol expected
213
+ })
214
+ end
215
+ end
216
+
217
+ ##### use it
218
+ a = SomeMiddleware.new
219
+ a.create_client('eder')
25
220
 
26
- For more information on how to use take a look into the specs.
27
221
 
28
222
  == Salesking API Resources
29
223
 
@@ -34,7 +228,10 @@ Right now the following resources are available:
34
228
  * products
35
229
  * credit_notes
36
230
 
37
- == Tests
231
+ See classes here:
232
+ http://github.com/salesking/sk-api/tree/master/lib/resources/
233
+
234
+ == Tests / Specs
38
235
 
39
236
  To run the test you must insert some credentials into the spec helper.
40
237
  We will gladly provide you with a test account on one of our development
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.6
1
+ 1.1.0
File without changes
@@ -27,7 +27,7 @@ module ActiveResource
27
27
  end
28
28
  end
29
29
 
30
- # Ooverridden to grab the data(= clients-collection) from json:
30
+ # Overridden to grab the data(= clients-collection) from json:
31
31
  # { 'collection'=> will_paginate infos,
32
32
  # 'links' => prev/next links
33
33
  # 'clients'=> [data], << what we need
@@ -37,15 +37,4 @@ module ActiveResource
37
37
  collection.collect! { |record| instantiate_record(record, prefix_options) }
38
38
  end
39
39
  end
40
- end
41
-
42
- # Force json decoding using Rufus
43
- module ActiveResource
44
- module Formats
45
- module JsonFormat
46
- def decode(json)
47
- Rufus::Json.decode(json)
48
- end
49
- end
50
- end
51
40
  end
@@ -1,5 +1,5 @@
1
1
  module ActiveResource
2
-
2
+
3
3
  module Validations
4
4
  # Validate a resource and save (POST) it to the remote web service.
5
5
  def save_with_validation
@@ -13,7 +13,7 @@ module ActiveResource
13
13
  errors.from_json(error.response.body)
14
14
  end
15
15
  false
16
- end
16
+ end
17
17
  end
18
18
 
19
19
  class Errors
@@ -0,0 +1,23 @@
1
+ module ActiveResource
2
+ # Overridden methods to suit SalesKing.
3
+ # Some changes might be kicked when AR 3.0 is out
4
+ class Base
5
+
6
+ # override ARes method to parse only the client part
7
+ def load_attributes_from_response(response)
8
+ if response['Content-Length'] != "0" && response.body.strip.size > 0
9
+ load( self.class.format.decode(response.body)[self.class.element_name] )
10
+ end
11
+ end
12
+
13
+ # Overridden to grab the data(= clients-collection) from json:
14
+ # { 'collection'=> will_paginate infos,
15
+ # 'links' => prev/next links
16
+ # 'clients'=> [data], << what we need
17
+ # }
18
+ def self.instantiate_collection(collection, prefix_options = {})
19
+ collection = collection[ self.element_name.pluralize ] if collection.is_a?(Hash)
20
+ collection.collect! { |record| instantiate_record(record, prefix_options) }
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,13 @@
1
+ module ActiveResource
2
+ class Errors < ActiveModel::Errors
3
+ # Patched cause we dont need no attribute name magic .. and its just simpler
4
+ # orig version is looking up the humanized name of the attribute in the error
5
+ # message, which we dont supply => only field name is used in returned error msg
6
+ def from_array(messages, save_cache=false)
7
+ clear unless save_cache
8
+ messages.each do |msg|
9
+ add msg[0], msg[1]
10
+ end
11
+ end
12
+ end #Errors
13
+ end
@@ -16,7 +16,7 @@ module SKApi
16
16
  "id" => {"type" => "string", "identity" => true, "readonly" => true},
17
17
  "address1" => {"type" => "string", "optional" => true},
18
18
  "address2" => {"type" => "string", "optional" => true},
19
- "city" => {"type" => "string", "optional" => true},
19
+ "city" => {"type" => "string"},
20
20
  "country" => {"type" => "string", "optional" => true},
21
21
  "state" => {"type" => "string", "optional" => true},
22
22
  "zip" => {"type" => "string", "optional" => true},
@@ -13,7 +13,7 @@ module SKApi
13
13
 
14
14
  def self.schema_props
15
15
  {
16
- "id" => {"type" => "string", "identity" => true , "readonly" => true},
16
+ "id" => {"type" => "string", "identity" => true, "optional" => true, "readonly" => true},
17
17
  "number" => {"type" => "string", "optional" => true},
18
18
  "organisation" => {"type" => "string"},
19
19
  "first_name" => {"type" => "string", "optional" => true},
@@ -45,7 +45,7 @@ module SKApi
45
45
  "created_at" => {"type" => "string", "format" =>"date-time", "optional" => true, "readonly" => true},
46
46
  "updated_at" => {"type" => "string", "format" =>"date-time", "optional" => true, "readonly" => true},
47
47
  "address_field" => {"type" => "string", "optional" => true, "readonly" => true},
48
- "addresses" => {"type" => "array"},
48
+ "addresses" => {"type" => "array","properties" => SKApi::Resources::Address.schema_props, "optional" => true}
49
49
  }
50
50
  end #schema_props
51
51
 
@@ -6,6 +6,19 @@ module SKApi
6
6
  save_with_validation
7
7
  end
8
8
 
9
+ def open
10
+ self.put(:open, :id => self.id) # PUT /invoices/:id/open
11
+ end
12
+
13
+ # rethink
14
+ def print(template_id)
15
+ self.get(:print, :template_id => template_id) # GET /invoices.pdf?template_id=:ids
16
+ end
17
+
18
+ def mail(template_id, cc, bcc)
19
+ self.get(:print, :template_id => template_id) # GET /invoices.pdf?template_id=:ids
20
+ end
21
+
9
22
  ##########################################################################
10
23
  # Class methods
11
24
  ##########################################################################
@@ -41,8 +54,14 @@ module SKApi
41
54
 
42
55
  def self.api_links
43
56
  #internal links on fields=> id => salesking.eu/clients/4567.json
44
- #external links to actions and related objects => invoeis => salesking.eu/clients/4567/invoices.json
45
57
  [:edit, :destroy, :copy, :print, :show, :payments, :payment_new]
58
+ # [ :edit,
59
+ # :destroy,
60
+ # { :copy =>"invoices/new?source=#{self.id}" },
61
+ # :open => "PUT invoices/#{self.id}/open",
62
+ # :print,
63
+ # :show, ]
64
+
46
65
  end
47
66
  end
48
67
  end
@@ -22,7 +22,6 @@ module SKApi
22
22
  "quantity_unit"=> {"type" => "string", "optional" => true},
23
23
  "tag_list" => {"type" => "string", "optional" => true},
24
24
  "lock_version" => {"type" => "integer", "readonly" => true, "optional" => true},
25
- "published_at" => {"type" => "string", "format" =>"date", "optional" => true},
26
25
  "created_at" => {"type" => "string", "format" =>"date-time", "optional" => true, "readonly" => true },
27
26
  "updated_at" => {"type" => "string", "format" =>"date-time", "optional" => true, "readonly" => true },
28
27
  }
data/lib/sk_api.rb CHANGED
@@ -1,29 +1,28 @@
1
- require 'rubygems'
1
+ #require 'rubygems'
2
2
  require 'active_resource'
3
+ require 'active_resource/version'
3
4
  # patches are for specific AR version
4
- #gem 'activeresource' , '=2.3.4'
5
+ # switch A-resource patch includes
5
6
 
6
- unless RUBY_PLATFORM =~ /java/
7
- require 'yajl'
8
- require 'rufus-json'
9
- Rufus::Json.backend = :yajl
10
- else
11
- require 'json'
12
- require 'rufus-json'
13
- Rufus::Json.backend = :json
7
+ if ActiveResource::VERSION::MAJOR == 3
8
+ require 'patches/ar3/base'
9
+ require 'patches/ar3/validations'
10
+ elsif ActiveResource::VERSION::MAJOR < 3
11
+ require 'patches/ar2/validations'
12
+ require 'patches/ar2/base'
14
13
  end
15
14
 
16
15
  # utilities
17
- require File.dirname(__FILE__) + '/utils/field_map'
18
- require File.dirname(__FILE__) + '/utils/serializer'
16
+ require 'utils/field_map'
17
+ require 'utils/serializer'
19
18
 
20
19
  #resources
21
- require File.dirname(__FILE__) + '/activeresource_patches/validations'
22
- require File.dirname(__FILE__) + '/activeresource_patches/base'
23
- require File.dirname(__FILE__) + '/resources/base'
24
- require File.dirname(__FILE__) + '/resources/product'
25
- require File.dirname(__FILE__) + '/resources/client'
26
- require File.dirname(__FILE__) + '/resources/address'
27
- require File.dirname(__FILE__) + '/resources/invoice'
28
- require File.dirname(__FILE__) + '/resources/credit_note'
29
- require File.dirname(__FILE__) + '/resources/line_item'
20
+ require 'resources/base'
21
+ require 'resources/product'
22
+ require 'resources/client'
23
+ require 'resources/address'
24
+ require 'resources/invoice'
25
+ require 'resources/credit_note'
26
+ require 'resources/line_item'
27
+
28
+ # TODO before post kick empty fields and readonly's
@@ -6,7 +6,7 @@ module SKApi
6
6
  # f.ex. in the clients api controller
7
7
  # => SKApi::Resources::Client.to_json(a_client)
8
8
  # This way you can keep your API client up to date by using the resources and
9
- # relying on SKApi::Resources::Client.api_fields
9
+ # relying on SKApi::Resources::Client.schema
10
10
  module Serializer
11
11
  def self.included(base)
12
12
  base.extend ClassMethods
@@ -42,8 +42,8 @@ module SKApi
42
42
  # ex: data['invoice']['client'] = SKApi::Models::Client.to_hash_from_schema(client)
43
43
  data[obj_class_name][field] = klass.to_hash_from_schema(rel_obj)
44
44
  end
45
- else # a simple field which can be directly called
46
- data[obj_class_name][field] = obj.send(field)
45
+ else # a simple field which can be directly called, only added of objects know its
46
+ data[obj_class_name][field] = obj.send(field) if obj.respond_to?(field.to_sym)
47
47
  end
48
48
  end
49
49
  data
@@ -52,7 +52,7 @@ module SKApi
52
52
  def to_json(obj)
53
53
  data = self.to_hash_from_schema(obj)
54
54
  # data[:links] = self.api_links
55
- Rufus::Json.encode(data)
55
+ ActiveSupport::JSON.encode(data)
56
56
  end
57
57
 
58
58
  end #ClassMethods
data/sk-api.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{sk-api}
8
- s.version = "1.0.6"
8
+ s.version = "1.1.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Georg Leciejewski"]
12
- s.date = %q{2010-04-16}
12
+ s.date = %q{2010-11-07}
13
13
  s.description = %q{Interact with SalesKing}
14
14
  s.email = %q{gl@salesking.eu}
15
15
  s.extra_rdoc_files = [
@@ -23,9 +23,11 @@ Gem::Specification.new do |s|
23
23
  "Rakefile",
24
24
  "VERSION",
25
25
  "init.rb",
26
- "lib/activeresource_patches/README",
27
- "lib/activeresource_patches/base.rb",
28
- "lib/activeresource_patches/validations.rb",
26
+ "lib/patches/README",
27
+ "lib/patches/ar2/base.rb",
28
+ "lib/patches/ar2/validations.rb",
29
+ "lib/patches/ar3/base.rb",
30
+ "lib/patches/ar3/validations.rb",
29
31
  "lib/resources/address.rb",
30
32
  "lib/resources/base.rb",
31
33
  "lib/resources/client.rb",
@@ -53,7 +55,7 @@ Gem::Specification.new do |s|
53
55
  s.homepage = %q{http://github.com/salesking/sk-api}
54
56
  s.rdoc_options = ["--charset=UTF-8"]
55
57
  s.require_paths = ["lib"]
56
- s.rubygems_version = %q{1.3.6}
58
+ s.rubygems_version = %q{1.3.7}
57
59
  s.summary = %q{Interact with SalesKing}
58
60
  s.test_files = [
59
61
  "spec/resources/client_spec.rb",
@@ -68,7 +70,7 @@ Gem::Specification.new do |s|
68
70
  current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
69
71
  s.specification_version = 3
70
72
 
71
- if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
73
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
72
74
  s.add_development_dependency(%q<rspec>, [">= 0"])
73
75
  else
74
76
  s.add_dependency(%q<rspec>, [">= 0"])
@@ -1,4 +1,4 @@
1
- require "#{File.dirname(__FILE__)}/../spec_helper"
1
+ require 'spec/spec_helper'
2
2
 
3
3
  describe SKApi::Resources::Client, "in general" do
4
4
 
@@ -59,9 +59,10 @@ describe SKApi::Resources::Client, "in general" do
59
59
  client = SKApi::Resources::Client.find(@client.id)
60
60
  # convert to json and read raw without activeresource assigning classes
61
61
  json = client.to_json
62
- obj = Rufus::Json.decode(json)
62
+ obj = ActiveSupport::JSON.decode(json)
63
+ # puts obj['client']['organisation']
63
64
  lambda {
64
- JSON::Schema.validate(obj, SKApi::Resources::Client.schema)
65
+ JSON::Schema.validate(obj['client'], SKApi::Resources::Client.schema)
65
66
  }.should_not raise_error
66
67
  end
67
68
 
@@ -83,6 +84,7 @@ describe SKApi::Resources::Client, "with addresses" do
83
84
  #setup test client to work with
84
85
  @client = SKApi::Resources::Client.new(:organisation=>'Second from testing API2',
85
86
  :addresses => [{ :zip => '50374', :city => 'Cologne' }] )
87
+
86
88
  @client.save
87
89
  end
88
90
 
@@ -1,4 +1,4 @@
1
- require "#{File.dirname(__FILE__)}/../spec_helper"
1
+ require 'spec/spec_helper'
2
2
 
3
3
  describe SKApi::Resources::CreditNote, "in general" do
4
4
 
@@ -25,11 +25,12 @@ describe SKApi::Resources::CreditNote, "in general" do
25
25
  }.should raise_error(ActiveResource::ResourceNotFound)
26
26
  end
27
27
 
28
- it "should create a doc" do
28
+ it "should create a doc and use default before after text" do
29
29
  @doc.errors.should be_empty
30
+ @doc.notes_before.should_not be_empty
30
31
  @doc.new?.should be_false
31
32
  end
32
-
33
+
33
34
  it "should fail create a doc" do
34
35
  doc = SKApi::Resources::CreditNote.new()
35
36
  doc.save.should == false
@@ -44,12 +45,11 @@ describe SKApi::Resources::CreditNote, "in general" do
44
45
 
45
46
  it "should validate raw json object with schema" do
46
47
  doc = SKApi::Resources::CreditNote.find(@doc.id)
47
- # doc.number.should=='23'
48
48
  # convert to json and read raw without activeresource assigning classes
49
49
  json = doc.to_json
50
- obj = Rufus::Json.decode(json)
50
+ obj = ActiveSupport::JSON.decode(json)
51
51
  lambda {
52
- JSON::Schema.validate(obj, SKApi::Resources::CreditNote.schema)
52
+ JSON::Schema.validate(obj['credit_note'], SKApi::Resources::CreditNote.schema)
53
53
  }.should_not raise_error
54
54
  end
55
55