sk_sdk 0.0.7 → 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -13,6 +13,7 @@ Dependencies (gem's):
13
13
  * activesupport
14
14
  * activeresource
15
15
  * curb
16
+ * sk_api_schema
16
17
 
17
18
  == Classes
18
19
 
@@ -22,22 +23,36 @@ All classes must be explicitly required so each can be used on their own.
22
23
  require 'sk_sdk/oauth'
23
24
  require 'sk_sdk/base'
24
25
 
25
- === API client
26
+ === API client Base
26
27
 
27
28
  Uses ActiveResource to CRUD SalesKing object's {see usage in README}[https://github.com/salesking/sk_sdk/blob/master/lib/sk_sdk/README_Base.rdoc]
28
29
 
29
30
  === oAuth
30
31
 
31
- Handling oAuth related URL's and getting an access token
32
+ Handling oAuth related URL's and getting an access token.
33
+ There also is an omniAuth strategy in case you are using it.
32
34
 
33
35
  === SignedRequest
34
36
 
35
37
  Helping in de/encoding of signed_request parameter available in canvas pages and
36
38
  PubSub/Webhook callbacks.
37
39
 
40
+ === Sync
41
+
42
+ Synchronize a local with an remote object. Tackles the problem of normalizing
43
+ objects to match internal data structures. Synchronization is done by a
44
+ field-mapping which also supports transition methods for each way.
45
+
38
46
  == Usage
39
47
 
40
48
  Read specs: https://github.com/salesking/sk_sdk/tree/master/spec/sk_sdk
41
49
 
50
+ == Tests
51
+
52
+ Before you run the tests use bundler to install all required gems:
53
+ # git clone
54
+ # cd into dir
55
+ BUNDLE_GEMFILE=ci/Gemfile bundle install
56
+ BUNDLE_GEMFILE=ci/Gemfile bundle exec rake spec
42
57
 
43
- Copyright (c) 2011 Georg Leciejewski, released under the MIT license
58
+ Copyright (c) 2011 Georg Leciejewski, released under the MIT license
data/Rakefile CHANGED
@@ -1,23 +1,23 @@
1
1
  require 'rubygems'
2
2
  require 'rake'
3
- require 'rake/rdoctask'
4
- require 'spec/rake/spectask'
3
+ require 'rdoc/task'
4
+ require 'rspec'
5
+ require 'rspec/core/rake_task'
5
6
 
6
7
  begin
7
8
  require 'jeweler'
8
9
  Jeweler::Tasks.new do |gem|
9
10
  gem.name = "sk_sdk"
10
11
  gem.summary = %Q{SalesKing SDK Ruby}
11
- gem.description = %Q{Connect your business world with SalesKing. This gem gives ruby developers a jump-start for building SalesKing Business Apps. Under the hood it provides classes to handle oAuth, make RESTfull API requests and parses JSON Schema }
12
+ gem.description = %Q{Connect your business world with SalesKing. This gem gives ruby developers a jump-start for building SalesKing Business Apps. It provides classes to handle oAuth, make RESTfull API requests and parses JSON Schema }
12
13
  gem.email = "gl@salesking.eu"
13
14
  gem.homepage = "http://github.com/salesking/sk_sdk"
14
15
  gem.authors = ["Georg Leciejewski"]
15
16
  gem.add_dependency 'curb'
16
17
  gem.add_dependency 'activesupport'
17
- # gem.add_dependency 'sk_api_schema'
18
- # gem.add_dependency 'sk_api_builder'
19
- # gem.add_dependency 'activeresource'
20
- gem.add_development_dependency "rspec", "< 2"
18
+ gem.add_dependency 'sk_api_schema'
19
+ gem.add_dependency 'activeresource'
20
+ gem.add_development_dependency "rspec"
21
21
  gem.add_development_dependency "rcov"
22
22
  end
23
23
  Jeweler::GemcutterTasks.new
@@ -28,19 +28,17 @@ end
28
28
  desc 'Default: run specs.'
29
29
  task :default => :spec
30
30
 
31
- spec_files = Rake::FileList["spec/**/*_spec.rb"]
32
-
33
31
  desc "Run specs"
34
- Spec::Rake::SpecTask.new do |t|
35
- t.spec_files = spec_files
36
- t.spec_opts = ["-c"]
32
+ RSpec::Core::RakeTask.new do |t|
33
+ t.pattern = "./spec/**/*_spec.rb" # don't need this, it's default.
34
+ # Put spec opts in a file named .rspec in root
37
35
  end
38
36
 
39
37
  desc "Generate code coverage"
40
- Spec::Rake::SpecTask.new(:coverage) do |t|
41
- t.spec_files = spec_files
38
+ RSpec::Core::RakeTask.new(:coverage) do |t|
39
+ t.pattern = "./spec/**/*_spec.rb" # don't need this, it's default.
42
40
  t.rcov = true
43
- t.rcov_opts = ['--exclude', 'spec,/var/lib/gems,/usr/local/lib']
41
+ t.rcov_opts = ['--exclude', 'spec']
44
42
  end
45
43
 
46
44
  desc 'Generate documentation.'
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.7
1
+ 0.0.8
data/ci/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source :gemcutter
2
+ gem "rake"
3
+ gem "rdoc"
4
+ gem "rcov"
5
+ gem "activesupport"
6
+ gem "activeresource"
7
+ gem "sk_api_schema"
8
+ gem "curb"
9
+ group :test do
10
+ gem "rspec"
11
+ end
@@ -1,15 +1,13 @@
1
1
  = SalesKing API Client
2
2
 
3
- ActiveResource based SalesKing API client.
4
- This does NOT rely on parsing the JSON Schema, since ActiveResource creates the
5
- Getter/Setter methods(less restrict). The client also adds some patches to AR to
6
- fix json parsing issues.
3
+ Easily access SalesKing resources in a RESTfull way with this ActiveResource
4
+ based SalesKing API client.
7
5
 
8
6
  == Install
9
7
 
10
8
  gem install sk_sdk
11
9
 
12
- If you are using bundler add to Gemfile
10
+ For bundler add this to the Gemfile
13
11
 
14
12
  gem "sk_sdk"
15
13
  # or directly require a used class
@@ -22,15 +20,19 @@ Dependencies (gem's):
22
20
 
23
21
  == Authorization
24
22
 
25
- This client should be used with an oAuth2 access_token, or as long as we support
26
- it can use HTTBasic Auth with username and password. For a quick start BasicAuth
27
- is definitly easier, but it is less secure since you cannot track which client
23
+ This client should be used with an oAuth2 access_token. But you can still use
24
+ HTTBasic Auth(username & password). For a quick start BasicAuth is definitely
25
+ easier, but it is less secure since you cannot track which client
28
26
  is accessing your account and the client has the full rights of the user. So if
29
27
  someone gets your credentials he can log into your SalesKing account and do
30
28
  whatever you can!
31
29
 
30
+ For a production environment using HTTP BasicAuth you should create one user
31
+ per api client and restrict his rights with our role-system!
32
+
32
33
  Getting an access_token or checking is validity is not the scope of this
33
- library, it merely sets the AUTHORIZATION header field to
34
+ library, it merely sets an AUTHORIZATION header if you added a token in the
35
+ connection settings.
34
36
  AUTHORIZATION: Bearer YourAccessToken
35
37
  See our {oAuth helper class}[https://github.com/salesking/sk_sdk/blob/master/lib/sk_sdk/oauth.rb]
36
38
  and read our {oAuth docs}[http://dev.blog.salesking.eu/docs/authentication/].
@@ -40,81 +42,85 @@ and read our {oAuth docs}[http://dev.blog.salesking.eu/docs/authentication/].
40
42
  SalesKing's api interface is RESTful(mostly) and returns & accepts JSON data.
41
43
  All resources such as clients, invoices, products can be accessed via URL's
42
44
  through standard HTTP methods GET, POST, PUT and DELETE.
43
- see available objects and endpoints here:
44
- https://github.com/salesking/sk_api_schema/tree/master/json
45
45
 
46
- First create the classes, which simply need to descend from SK::SDK::Base
47
- There are several ways to do so:
46
+ The available objects and their endpoints(links) are described in {our json-schema}[https://github.com/salesking/sk_api_schema/tree/master/json].
47
+ Take a look into the links-section of each schema to see available
48
+ endpoints/routes and their parameter f.ex. to apply search filtering
49
+
50
+ Your classes simply descend from SK::SDK::Base and need the connection settings.
51
+ You MUST provide the full URL using the right protocol in the connection and
52
+ remember our production system only supports HTTPS:
53
+ https + SUBDomain + salesking url + /api
54
+
55
+ Create a single class
48
56
 
49
57
  require 'sk_sdk/base'
50
- # simple object creating
58
+
51
59
  class Client < SK::SDK::Base;end
52
60
 
53
- # create objects in custom King namespace
61
+ # connection using BasicAuth
62
+ Client.set_connection {:site => 'https://my_sub.salesking.eu/api',
63
+ :user => 'my-users@login-email.com',
64
+ :password => 'password' })
65
+
66
+ client = Client.new(:last_name => 'Ding', :organisation => "Carpenters Inc.")
67
+ client.first_name = "Bill"
68
+ client.save
69
+
70
+
71
+ Create multiple classes at once
72
+
73
+ require 'sk_sdk/base'
54
74
  module; King; end
55
75
  %w[Client Invoice Product LineItem].each do |model|
56
76
  eval "class King::#{model} < SK::SDK::Base;end"
57
77
  end
58
78
 
59
- # add your connection settings
79
+ # connection using oAuth access token
60
80
  SK::SDK::Base.set_connection {:site => 'https://my_sub.salesking.eu/api',
61
- :user => 'my-users@login-email.com',
62
- :password => 'password' })
63
- #OR Use an oAuth access token
64
- #SK::SDK::Base.set_connection {:site => 'https://my_sub.salesking.eu/api',
65
- # :token => 'someAccessToken'})
66
-
67
- # use it
68
- client = Client.new(:last_name=> 'Meister')
69
- client.first_name = "Bau"
70
- client.save
71
- credit_note = King::CreditNote.new
72
-
73
- You MUST provide the full url using the right protocol when you set the
74
- connection and remember our production system only supports HTTPS:
75
- http/https + SUBDomain + salesking url + /api
76
-
77
- The old ArCli.make method to create the classes, still exists but is deprecated:
81
+ :token => 'someAccessToken'})
78
82
 
79
- require "sk_sdk/ar_cli"
80
- SK::SDK::ArCli.make(:client)
81
-
82
- # OR create a class within a namespace
83
- module; King; end
84
- SK::SDK::ArCli.make(:credit_note, King)
83
+ invoice = Invoice.new
84
+ invoice.title = "Hosting 2011"
85
+ item = LineItem.new { :position=>1, :name => 'Daily Backup',
86
+ :quantity_unit=> 'Month', :quantity => 12,
87
+ :price_single => 10.00 }
88
+ invoice.line_items = [ item ]
89
+ invoice.status = 'open'
90
+ invoice.save
85
91
 
86
92
 
87
93
  Want to know more about REST style webservices?
88
94
  * http://en.wikipedia.org/wiki/Representational_State_Transfer
89
95
  * http://www.google.com/search?q=REST+site%3Awww.infoq.com
90
96
 
91
- === Authentification & Safety
92
-
93
- Authentification can be done with oAuth2 but we still support HTTP BasicAuth
94
- using your SalesKing login email and password. oAuth examples(support) will
95
- follow.
96
-
97
- For a production environment be advised to create a user, per api client, and
98
- restrict his rights with our build in role-system!
97
+ == Hints on ActiveResource
99
98
 
99
+ * Most of the magic is coming from ActiveResource so you should read {its README and code}[https://github.com/rails/rails/tree/master/activeresource]
100
+ * This client does NOT rely on parsing the JSON Schema, since ActiveResource(AR) creates the Getter/Setter methods.
101
+ * We added some patches for AR to fix JSON parsing issues, due to our nesting.
102
+ * non-restful routes can be accessed by custom methods {see examples in AR}[https://github.com/rails/rails/blob/master/activeresource/lib/active_resource/custom_methods.rb]
103
+ Invoice.find('uuid').post(:print, :template_id => 'pdf-template-uuid')
100
104
 
101
- === API Tutorial and Tools
105
+ == Tutorials & Tools
102
106
 
103
- Since browsers do not support PUT/DELETE methods you can use CURL, a linux
104
- command-line http client, for testing. And of course any http library supporting
105
- http-basic-auth can be used.
107
+ Since browsers do not support PUT/DELETE methods you can use CURL(a linux
108
+ command-line http client) for testing. And of course any http library supporting
109
+ http-basic-auth.
106
110
 
107
- * Getting started: http://dev.blog.salesking.eu/api/
108
- * ActiveResource readme: https://github.com/rails/rails/tree/master/activeresource
109
- * Chrome cRest extension https://chrome.google.com/extensions/detail/baedhhmoaooldchehjhlpppaieoglhml
110
- * Poster FF-Plugin - make HTTP request https://addons.mozilla.org/en-US/firefox/addon/2691/ (you must be logged into SalesKing)
111
- * JSONView FF-Plugin - view json in firefox https://addons.mozilla.org/de/firefox/addon/10869/
112
- * JSONovich FF-Plugin - https://addons.mozilla.org/de/firefox/addon/10122/
111
+ * {Getting started tutorial}[http://dev.blog.salesking.eu/api/]
112
+ * {SalesKing API Schema}[https://github.com/salesking/sk_api_schema]
113
+ * {Chrome cRest extension}[https://chrome.google.com/extensions/detail/baedhhmoaooldchehjhlpppaieoglhml]
114
+ * {Poster FF-Plugin - make HTTP request}[https://addons.mozilla.org/en-US/firefox/addon/2691/] (you must be logged into SalesKing)
115
+ * {JSONView FF-Plugin - view json in firefox}[https://addons.mozilla.org/de/firefox/addon/10869/]
116
+ * {JSONovich FF-Plugin}[https://addons.mozilla.org/de/firefox/addon/10122/]
113
117
 
114
118
  == Tests / Specs
115
119
 
116
- Take a look into spec/resources to see a how-to
120
+ Please read the tests as they provide some more examples especially those in
121
+ spec/resources
117
122
 
123
+ Run the specs with:
118
124
  rake coverage
119
125
 
120
126
  Copyright (c) 2011 Georg Leciejewski, released under the MIT license
data/lib/sk_sdk/base.rb CHANGED
@@ -4,11 +4,11 @@ require 'active_resource'
4
4
  require 'active_resource/version'
5
5
  # patches are for specific AR version
6
6
  if ActiveResource::VERSION::MAJOR == 3
7
- require 'sk_sdk/ar_cli/patches/ar3/base'
8
- require 'sk_sdk/ar_cli/patches/ar3/validations'
7
+ require 'sk_sdk/ar_patches/ar3/base'
8
+ require 'sk_sdk/ar_patches/ar3/validations'
9
9
  elsif ActiveResource::VERSION::MAJOR < 3
10
- require 'sk_sdk/ar_cli/patches/ar2/validations'
11
- require 'sk_sdk/ar_cli/patches/ar2/base'
10
+ require 'sk_sdk/ar_patches/ar2/validations'
11
+ require 'sk_sdk/ar_patches/ar2/base'
12
12
  end
13
13
 
14
14
  class SK::SDK::Base < ActiveResource::Base
data/lib/sk_sdk/oauth.rb CHANGED
@@ -53,8 +53,7 @@ module SK::SDK
53
53
 
54
54
  # URL to get the access_token, used in the second step after you have
55
55
  # requested the authorization and gotten a code
56
- # The token url is located at /oauth/token like proposed in draft oAuth2.16
57
- # but can still be reached at /access_token so older libs still work
56
+ # The token url is located at /oauth/token
58
57
  # === Parameter
59
58
  # code<String>:: code received after auth
60
59
  # === Returns
@@ -17,7 +17,7 @@ If you are using bundler add to Gemfile
17
17
 
18
18
  gem "sk_sdk"
19
19
  # or directly require used class
20
- gem "sk_sdk", :require => "sk_sdk/ar_omni_auth/salesking"
20
+ gem "sk_sdk", :require => "sk_sdk/omni_auth/salesking"
21
21
 
22
22
  == Usage
23
23
 
@@ -15,7 +15,8 @@ module OmniAuth
15
15
  def initialize(app, client_id, client_secret, sk_url, scope)
16
16
  @base_url = sk_url
17
17
  @scope = scope
18
- super(app, :salesking, client_id, client_secret)
18
+ client_options = {:access_token_path => '/oauth/token'}
19
+ super(app, :salesking, client_id, client_secret, client_options)
19
20
  end
20
21
 
21
22
  #inject salesking url and scope
@@ -0,0 +1,170 @@
1
+ module SK::SDK
2
+
3
+ # Provide methods for mapping and syncing the fields of a remote to local
4
+ # object.
5
+ # The class gets two objects(local/remote) and a field-map(Array) which
6
+ # maps the field-names between those two. Of course your objects MUST respond
7
+ # to the method names passed in the mapping.
8
+ #
9
+ # When syncing the corresponding fields, the names are simply #send to each
10
+ # object.
11
+ #
12
+ # After an object was updated you can check the #log for changes
13
+ #
14
+ # == Example
15
+ #
16
+ # map =[
17
+ # [:name, :full_name, :'someClass.set_local_name', :'SomeClass.set_remote_name'],
18
+ # [:street, :address1]
19
+ # ]
20
+ # map = SK::SDK::Sync.new(@local_user, @remote_user, map)
21
+ # map.update(:r) #Does not save! only sets the field values on the remote object
22
+ #
23
+ # == Mapping Explanation
24
+ #
25
+ # A mapping consist of the local and the remote field name. It can further
26
+ # contain transition methods if the value needs to be changed when set from
27
+ # one side to the other.
28
+ #
29
+ # Those methods will be called(eval'ed) and receive the value from the other
30
+ # side as param:
31
+ #
32
+ # Mappings are passed as an array:
33
+ # [
34
+ # [:local_field_name, :remote_field_name, "SomeClass.left_trans", "SomeClass.rigt_trans"]
35
+ # [:firstname, :first_name, :'SomeClass.set_local_name', :'SomeClass.set_remote_name']
36
+ # ]
37
+ class Sync
38
+
39
+ # The local object
40
+ attr_accessor :l_obj
41
+ # The remote object
42
+ attr_accessor :r_obj
43
+ # <Hash{Symbol=>Symbol, Symbol=>{Hash} }>::the field mapping
44
+ attr_reader :fields
45
+ # the outdated fields
46
+ attr_reader :outdated
47
+ # <Array[String]>::log field changes
48
+ attr_reader :log
49
+
50
+ # Takes a local and remote object which should respond to function defined
51
+ # in the mapping hash
52
+ # === Parameter
53
+ # l_obj<Object>::
54
+ # r_obj<Object>::
55
+ # field_map<Hash{Symbol=>Symbol, Symbol=>{Hash} }>::the field mapping
56
+ def initialize(l_obj, r_obj, field_map)
57
+ @l_obj = l_obj
58
+ @r_obj = r_obj
59
+ self.fields = field_map
60
+ @log = []
61
+ end
62
+
63
+ # === Parameter
64
+ # field_map<Array[Hash{}]>::
65
+ def fields=(field_map)
66
+ @fields = []
67
+ field_map.each do |fld|
68
+ @fields << Field.new(fld)
69
+ end
70
+ @fields
71
+ end
72
+
73
+ # Find a field by its local name
74
+ # === Parameter
75
+ # l_name<Symbol>:: local name
76
+ # === Return
77
+ # <Field>::
78
+ def field(l_name)
79
+ fields.find{|fld| fld.l_name == l_name}
80
+ end
81
+
82
+ # Check if the any of the fields are outdated
83
+ # Populates self.outdated with outdated local field names
84
+ # === Returns
85
+ # <Boolean>:: false if not outdated
86
+ def outdated?
87
+ @outdated = []
88
+ fields.each do |fld|
89
+ if fld.transition?
90
+ # call r_trans method with local val to compare local with remote val
91
+ # SomeTrans.remote_transfer_method( l_obj.field )
92
+ virtual_l_val = eval "#{fld.r_trans} l_obj.send( fld.l_name )"
93
+ @outdated << fld if virtual_l_val != r_obj.send( fld.r_name )
94
+ else
95
+ # no transfer method, directly compare values
96
+ @outdated << fld if r_obj.send( fld.r_name ) != l_obj.send( fld.l_name )
97
+ end
98
+ end
99
+ !@outdated.empty?
100
+ end
101
+
102
+ # update all local outdated fields with values from remote object
103
+ def update_local_outdated
104
+ update(:l, @outdated) if outdated?
105
+ end
106
+ # update all remote outdated fields with values from local object
107
+ def update_remote_outdated
108
+ update( :r, @outdated) if outdated?
109
+ end
110
+
111
+ # Update a side with the values from the other side.
112
+ # Populates the log with updated fields and values.
113
+ # === Parameter
114
+ # side<String|Symbol>:: the side to update l OR r
115
+ # flds<Array[Field] | nil>:: fields to update, if nil all fields are updated
116
+ def update(side, flds=nil)
117
+ raise ArgumentError, 'The side to update must be :l or :r' unless [:l, :r].include?(side)
118
+ target, source = (side==:l) ? [:l, :r] : [:r, :l]
119
+ # use set field/s or update all
120
+ flds ||= fields
121
+ target_obj, source_obj = self.send("#{target}_obj"), self.send("#{source}_obj")
122
+ flds.each do |fld|
123
+ target_name, source_name = fld.send("#{target}_name"), fld.send("#{source}_name")
124
+ # remember for log
125
+ old_val = target_obj.send(target_name) rescue 'empty'
126
+ # get new value through transfer method or direct
127
+ new_val = if fld.transition? #call transfer function
128
+ cur_trans = fld.send("#{target}_trans")
129
+ eval "#{cur_trans} source_obj.send( source_name )"
130
+ else # lookup directly on other side object
131
+ source_obj.send( source_name )
132
+ end
133
+ target_obj.send( "#{target_name}=" , new_val )
134
+
135
+ log << "#{target_name} was: #{old_val} updated from: #{source_name} with value: #{new_val}"
136
+ end
137
+ end
138
+
139
+ # A Sync::Field holds the local(left) and remote(right) field names and if
140
+ # available the transfer methods.
141
+ class Field
142
+ attr_reader :l_name, :r_name, :l_trans, :r_trans
143
+
144
+ # Create a new sync field. the local and remote name MUST be set.
145
+ # Transition methods are optional.
146
+ #
147
+ # == Example
148
+
149
+ # With options as array:
150
+ # opts = [:local_name, :remote_name, "SomeClass.left_trans", "SomeClass.rigt_trans"]
151
+ # fld = Field.new opts
152
+ #
153
+ # == Parameter
154
+ # opts<Hash>::
155
+ def initialize(opts)
156
+ if opts.is_a? Array
157
+ @l_trans, @r_trans = opts[2], opts[3] if opts.length == 4
158
+ @l_name = opts[0]
159
+ @r_name = opts[1]
160
+ end
161
+ end
162
+
163
+ def transition?
164
+ @l_trans && @r_trans
165
+ end
166
+ end # class Field
167
+
168
+
169
+ end # Sync
170
+ end
data/sk_sdk.gemspec CHANGED
@@ -5,12 +5,12 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{sk_sdk}
8
- s.version = "0.0.7"
8
+ s.version = "0.0.8"
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{2011-06-03}
13
- s.description = %q{Connect your business world with SalesKing. This gem gives ruby developers a jump-start for building SalesKing Business Apps. Under the hood it provides classes to handle oAuth, make RESTfull API requests and parses JSON Schema }
12
+ s.date = %q{2011-09-02}
13
+ s.description = %q{Connect your business world with SalesKing. This gem gives ruby developers a jump-start for building SalesKing Business Apps. It provides classes to handle oAuth, make RESTfull API requests and parses JSON Schema }
14
14
  s.email = %q{gl@salesking.eu}
15
15
  s.extra_rdoc_files = [
16
16
  "README.rdoc"
@@ -20,22 +20,22 @@ Gem::Specification.new do |s|
20
20
  "README.rdoc",
21
21
  "Rakefile",
22
22
  "VERSION",
23
+ "ci/Gemfile",
23
24
  "lib/sk_sdk.rb",
24
25
  "lib/sk_sdk/README_Base.rdoc",
25
- "lib/sk_sdk/ar_cli.rb",
26
- "lib/sk_sdk/ar_cli/patches/ar2/base.rb",
27
- "lib/sk_sdk/ar_cli/patches/ar2/validations.rb",
28
- "lib/sk_sdk/ar_cli/patches/ar3/base.rb",
29
- "lib/sk_sdk/ar_cli/patches/ar3/validations.rb",
26
+ "lib/sk_sdk/ar_patches/ar2/base.rb",
27
+ "lib/sk_sdk/ar_patches/ar2/validations.rb",
28
+ "lib/sk_sdk/ar_patches/ar3/base.rb",
29
+ "lib/sk_sdk/ar_patches/ar3/validations.rb",
30
30
  "lib/sk_sdk/base.rb",
31
31
  "lib/sk_sdk/oauth.rb",
32
32
  "lib/sk_sdk/omni_auth/README.rdoc",
33
33
  "lib/sk_sdk/omni_auth/salesking.rb",
34
34
  "lib/sk_sdk/signed_request.rb",
35
+ "lib/sk_sdk/sync.rb",
35
36
  "sk_sdk.gemspec",
36
37
  "spec/resources_spec_helper.rb",
37
38
  "spec/settings.yml",
38
- "spec/sk_sdk/ar_cli_spec.rb",
39
39
  "spec/sk_sdk/base_spec.rb",
40
40
  "spec/sk_sdk/oauth_spec.rb",
41
41
  "spec/sk_sdk/resources/README.rdoc",
@@ -44,6 +44,8 @@ Gem::Specification.new do |s|
44
44
  "spec/sk_sdk/resources/invoice_spec.rb",
45
45
  "spec/sk_sdk/resources/product_spec.rb",
46
46
  "spec/sk_sdk/signed_request_spec.rb",
47
+ "spec/sk_sdk/sync_field_spec.rb",
48
+ "spec/sk_sdk/sync_spec.rb",
47
49
  "spec/spec_helper.rb"
48
50
  ]
49
51
  s.homepage = %q{http://github.com/salesking/sk_sdk}
@@ -57,18 +59,24 @@ Gem::Specification.new do |s|
57
59
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
58
60
  s.add_runtime_dependency(%q<curb>, [">= 0"])
59
61
  s.add_runtime_dependency(%q<activesupport>, [">= 0"])
60
- s.add_development_dependency(%q<rspec>, ["< 2"])
62
+ s.add_runtime_dependency(%q<sk_api_schema>, [">= 0"])
63
+ s.add_runtime_dependency(%q<activeresource>, [">= 0"])
64
+ s.add_development_dependency(%q<rspec>, [">= 0"])
61
65
  s.add_development_dependency(%q<rcov>, [">= 0"])
62
66
  else
63
67
  s.add_dependency(%q<curb>, [">= 0"])
64
68
  s.add_dependency(%q<activesupport>, [">= 0"])
65
- s.add_dependency(%q<rspec>, ["< 2"])
69
+ s.add_dependency(%q<sk_api_schema>, [">= 0"])
70
+ s.add_dependency(%q<activeresource>, [">= 0"])
71
+ s.add_dependency(%q<rspec>, [">= 0"])
66
72
  s.add_dependency(%q<rcov>, [">= 0"])
67
73
  end
68
74
  else
69
75
  s.add_dependency(%q<curb>, [">= 0"])
70
76
  s.add_dependency(%q<activesupport>, [">= 0"])
71
- s.add_dependency(%q<rspec>, ["< 2"])
77
+ s.add_dependency(%q<sk_api_schema>, [">= 0"])
78
+ s.add_dependency(%q<activeresource>, [">= 0"])
79
+ s.add_dependency(%q<rspec>, [">= 0"])
72
80
  s.add_dependency(%q<rcov>, [">= 0"])
73
81
  end
74
82
  end
@@ -1,6 +1,8 @@
1
1
  CONNECTION = {
2
- :site => "http://demo.salesking.local:3000/api/",
3
- :password => "demo",
2
+ :site => "https://demo.dev.salesking.eu/api/",
3
+ :password => "demouser",
4
+ # :site => "http://demo.salesking.local:3000/api/",
5
+ # :password => "demo",
4
6
  :user => "demo@salesking.eu"
5
7
  } unless defined?(CONNECTION)
6
8
 
@@ -1,5 +1,5 @@
1
- require 'spec/spec_helper'
2
- require 'spec/resources_spec_helper'
1
+ require 'spec_helper'
2
+ require 'resources_spec_helper'
3
3
 
4
4
  class Client < SK::SDK::Base;end
5
5
  # create objects in King namespace
@@ -1,4 +1,4 @@
1
- require 'spec/spec_helper'
1
+ require 'spec_helper'
2
2
 
3
3
  describe SK::SDK::Oauth, "in general" do
4
4
 
@@ -1,5 +1,5 @@
1
- require 'spec/spec_helper'
2
- require 'spec/resources_spec_helper'
1
+ require 'spec_helper'
2
+ require 'resources_spec_helper'
3
3
 
4
4
  unless sk_available?
5
5
  puts "Sorry cannot connect to your SalesKing server, skipping real connections tests. Please check connection settings in spec_helper"
@@ -1,5 +1,5 @@
1
- require 'spec/spec_helper'
2
- require 'spec/resources_spec_helper'
1
+ require 'spec_helper'
2
+ require 'resources_spec_helper'
3
3
 
4
4
  unless sk_available?
5
5
  puts "Sorry cannot connect to your SalesKing server, skipping real connections tests. Please check connection settings in spec_helper"
@@ -36,11 +36,14 @@ else
36
36
  @doc.new?.should be_false
37
37
  end
38
38
 
39
- it "should fail create a doc" do
40
- doc = CreditNote.new()
41
- doc.save.should == false
42
- doc.errors.count.should == 1
43
- doc.errors.on(:client_id).should == "can't be blank"
39
+ it "should fail create a doc without unique number" do
40
+ doc = CreditNote.new(:number=>'001')
41
+ doc.save.should == true
42
+ doc2 = CreditNote.new(:number=>'001')
43
+ doc2.save.should == false
44
+ doc2.errors.count.should == 2
45
+ doc2.errors.on(:number).should == "has already been taken"
46
+ doc.destroy
44
47
  end
45
48
 
46
49
  it "should find a doc" do
@@ -58,11 +61,14 @@ else
58
61
  @doc.lock_version.should > old_lock_version # because save returns the data
59
62
  end
60
63
 
61
- it "should fail edit a doc" do
62
- @doc.client_id = ''
64
+ it "should fail edit with wrong number" do
65
+ doc1 = CreditNote.new(:number=>'002')
66
+ doc1.save.should == true
67
+ @doc.number = '002'
63
68
  @doc.save.should == false
64
- @doc.errors.count.should == 1
65
- @doc.errors.on(:client_id).should == "can't be blank"
69
+ @doc.errors.count.should == 2
70
+ @doc.errors.on(:number).should == "has already been taken"
71
+ doc1.destroy
66
72
  end
67
73
  end
68
74
 
@@ -1,5 +1,5 @@
1
- require 'spec/spec_helper'
2
- require 'spec/resources_spec_helper'
1
+ require 'spec_helper'
2
+ require 'resources_spec_helper'
3
3
 
4
4
  unless sk_available?
5
5
  puts "Sorry cannot connect to your SalesKing server, skipping real connections tests. Please check connection settings in spec_helper"
@@ -60,11 +60,14 @@ describe Invoice, "a new invoice" do
60
60
  doc.destroy
61
61
  end
62
62
 
63
- it "should fail create a doc" do
64
- doc = Invoice.new()
65
- doc.save.should == false
66
- doc.errors.count.should == 1
67
- doc.errors.on(:client_id).should == "can't be blank"
63
+ it "should fail create a doc without unique number" do
64
+ doc = Invoice.new(:number=>'001')
65
+ doc.save.should == true
66
+ doc2 = Invoice.new(:number=>'001')
67
+ doc2.save.should == false
68
+ doc2.errors.count.should == 2
69
+ doc2.errors.on(:number).should == "has already been taken"
70
+ doc.destroy
68
71
  end
69
72
 
70
73
  end
@@ -99,11 +102,14 @@ describe Invoice, "Edit an invoice" do
99
102
  @doc.notes_before.should == 'You will recieve the amout of:'
100
103
  end
101
104
 
102
- it "should fail edit without a client" do
103
- @doc.client_id = ''
105
+ it "should fail edit with wrong number" do
106
+ doc1 = Invoice.new(:number=>'002')
107
+ doc1.save.should == true
108
+ @doc.number = '002'
104
109
  @doc.save.should == false
105
- @doc.errors.count.should == 1
106
- @doc.errors.on(:client_id).should == "can't be blank"
110
+ @doc.errors.count.should == 2
111
+ @doc.errors.on(:number).should == "has already been taken"
112
+ doc1.destroy
107
113
  end
108
114
  end
109
115
 
@@ -1,5 +1,5 @@
1
- require 'spec/spec_helper'
2
- require 'spec/resources_spec_helper'
1
+ require 'spec_helper'
2
+ require 'resources_spec_helper'
3
3
 
4
4
  unless sk_available?
5
5
  puts "Sorry cannot connect to your SalesKing server, skipping real connections tests. Please check connection settings in spec_helper"
@@ -1,4 +1,4 @@
1
- require 'spec/spec_helper'
1
+ require 'spec_helper'
2
2
 
3
3
  describe SK::SDK::SignedRequest, "in general" do
4
4
 
@@ -12,9 +12,13 @@ describe SK::SDK::SignedRequest, "in general" do
12
12
 
13
13
  it "should decode payload" do
14
14
  a = SK::SDK::SignedRequest.new(@param, @set['secret'])
15
- a.data.should_not be_nil
15
+ a.data.should == @param_hash
16
16
  a.payload.should_not be_nil
17
17
  a.sign.should_not be_nil
18
+ end
19
+
20
+ it "should validate" do
21
+ a = SK::SDK::SignedRequest.new(@param, @set['secret'])
18
22
  a.should be_valid
19
23
  end
20
24
 
@@ -0,0 +1,41 @@
1
+ require 'spec/spec_helper'
2
+
3
+ describe SK::SDK::Sync::Field do
4
+
5
+ it "should init with array fields" do
6
+ flds = []
7
+ field_map.each do |fld|
8
+ flds << SK::SDK::Sync::Field.new(fld)
9
+ end
10
+ flds.length.should == field_map.length
11
+ flds.first.should be_a_kind_of SK::SDK::Sync::Field
12
+ end
13
+
14
+ it "should set names fields" do
15
+ opts = field_map.first
16
+ fld = SK::SDK::Sync::Field.new(opts)
17
+ fld.l_name.should == opts[0]
18
+ fld.r_name.should == opts[1]
19
+ end
20
+
21
+ it "should set transition methods" do
22
+ opts = field_map.last
23
+ fld = SK::SDK::Sync::Field.new(opts)
24
+ fld.l_trans.should == opts[2]
25
+ fld.r_trans.should == opts[3]
26
+ end
27
+
28
+
29
+
30
+ def field_map
31
+ [
32
+ [:firstname, :first_name],
33
+ [:street, :address1],
34
+ [:postcode, :zip],
35
+ [:city, :city],
36
+ [:gender, :gender, :'TransMethods.set_local_gender', :'TransMethods.set_remote_gender']
37
+ ]
38
+
39
+ end
40
+
41
+ end
@@ -0,0 +1,126 @@
1
+
2
+ require 'spec/spec_helper'
3
+
4
+ describe SK::SDK::Sync do
5
+
6
+ before :each do
7
+ @l_obj = LocalContact.new
8
+ @r_obj = RemoteContact.new
9
+ @sync = SK::SDK::Sync.new(@l_obj, @r_obj, field_map)
10
+ end
11
+
12
+ it "should create fields" do
13
+ @sync.fields.length.should == field_map.length
14
+ @sync.fields.first.should be_a_kind_of SK::SDK::Sync::Field
15
+ end
16
+
17
+ it "should raise error with wrong side" do
18
+ lambda{
19
+ @sync.update(:x)
20
+ }.should raise_error(ArgumentError)
21
+ end
22
+
23
+ it "should not be outdated" do
24
+ @sync.outdated?.should be_false # both objects are empty
25
+ end
26
+
27
+ it "should find outdated fields" do
28
+ @l_obj.firstname = 'theo'
29
+ @sync.outdated?.should be_true
30
+ @sync.outdated.first.should == @sync.field(:firstname)
31
+ end
32
+
33
+ it "should update outdated remote fields" do
34
+ @l_obj.firstname = 'theo'
35
+ @sync.update_remote_outdated
36
+
37
+ @r_obj.first_name.should == @l_obj.firstname
38
+ @sync.log.should_not be_empty
39
+ end
40
+
41
+ it "should update outdated local fields" do
42
+ @r_obj.first_name = 'Heinz'
43
+ @sync.update_local_outdated
44
+ @l_obj.firstname.should == @r_obj.first_name
45
+ @sync.log.length.should == 1
46
+ end
47
+
48
+ it "should update outdated remote fields with transition" do
49
+ @l_obj.gender = 'female'
50
+ @sync.update_remote_outdated
51
+
52
+ @r_obj.gender.should == "f"
53
+ @sync.log.should_not be_empty
54
+ end
55
+
56
+ it "should update outdated local fields with transition" do
57
+ @r_obj.gender = 'm'
58
+ @sync.update_local_outdated
59
+ @l_obj.gender.should == 'male'
60
+ @sync.log.length.should == 1
61
+ end
62
+
63
+ it "should update all remote fields" do
64
+ @l_obj.firstname = 'John'
65
+ @l_obj.street = 'Sync Ave 666'
66
+ @l_obj.postcode = '96969'
67
+ @l_obj.city = 'Wichita'
68
+ @l_obj.gender = 'female'
69
+ @sync.update(:r)
70
+
71
+ @r_obj.first_name.should == @l_obj.firstname
72
+ @r_obj.address1.should == @l_obj.street
73
+ @r_obj.zip.should == @l_obj.postcode
74
+ @r_obj.city.should == @l_obj.city
75
+ @r_obj.gender.should == "f"
76
+ @sync.log.should_not be_empty
77
+ end
78
+
79
+ it "should update all local fields" do
80
+ @r_obj.gender = 'm'
81
+ @r_obj.first_name = 'John'
82
+ @r_obj.address1 = 'Sync Ave 666'
83
+ @r_obj.zip = '96969'
84
+ @r_obj.city = 'Wichita'
85
+ @sync.update(:l)
86
+
87
+ @l_obj.firstname.should == @r_obj.first_name
88
+ @l_obj.street.should == @r_obj.address1
89
+ @l_obj.postcode.should == @r_obj.zip
90
+ @l_obj.city.should == @r_obj.city
91
+ @l_obj.gender.should == 'male'
92
+ @sync.log.length.should == 5
93
+ end
94
+ def field_map
95
+ [
96
+ [:firstname, :first_name],
97
+ [:street, :address1],
98
+ [:postcode, :zip],
99
+ [:city, :city],
100
+ [:gender, :gender, :'TransMethods.set_local_gender', :'TransMethods.set_remote_gender']
101
+ ]
102
+ end
103
+
104
+ end
105
+
106
+ ################################################################################
107
+ # Dummi Classes used in specs
108
+ ################################################################################
109
+ class RemoteContact
110
+ attr_accessor :first_name, :address1, :zip, :city, :gender
111
+ end
112
+
113
+ class LocalContact
114
+ attr_accessor :firstname, :street, :postcode, :city, :gender
115
+ end
116
+
117
+ class TransMethods
118
+ def self.set_local_gender(remote_val)
119
+ return 'male' if remote_val == 'm'
120
+ return 'female' if remote_val == 'f'
121
+ end
122
+ def self.set_remote_gender(local_val)
123
+ return 'm' if local_val == 'male'
124
+ return 'f' if local_val == 'female'
125
+ end
126
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,13 +1,13 @@
1
1
  require 'rubygems'
2
2
  require 'yaml'
3
- require 'spec'
3
+ require 'rspec'
4
4
  require "active_support"
5
5
  require "active_support/json"
6
6
  require "#{File.dirname(__FILE__)}/../lib/sk_sdk"
7
7
  require "#{File.dirname(__FILE__)}/../lib/sk_sdk/base"
8
+ require "#{File.dirname(__FILE__)}/../lib/sk_sdk/sync"
8
9
  require "#{File.dirname(__FILE__)}/../lib/sk_sdk/oauth"
9
10
  require "#{File.dirname(__FILE__)}/../lib/sk_sdk/signed_request"
10
- require "#{File.dirname(__FILE__)}/../lib/sk_sdk/ar_cli"
11
11
 
12
12
 
13
13
  puts "Testing with ActiveResource v: #{ActiveResource::VERSION::STRING}"
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sk_sdk
3
3
  version: !ruby/object:Gem::Version
4
- hash: 17
4
+ hash: 15
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 0
9
- - 7
10
- version: 0.0.7
9
+ - 8
10
+ version: 0.0.8
11
11
  platform: ruby
12
12
  authors:
13
13
  - Georg Leciejewski
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-06-03 00:00:00 +02:00
18
+ date: 2011-09-02 00:00:00 +02:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -47,21 +47,21 @@ dependencies:
47
47
  type: :runtime
48
48
  version_requirements: *id002
49
49
  - !ruby/object:Gem::Dependency
50
- name: rspec
50
+ name: sk_api_schema
51
51
  prerelease: false
52
52
  requirement: &id003 !ruby/object:Gem::Requirement
53
53
  none: false
54
54
  requirements:
55
- - - <
55
+ - - ">="
56
56
  - !ruby/object:Gem::Version
57
- hash: 7
57
+ hash: 3
58
58
  segments:
59
- - 2
60
- version: "2"
61
- type: :development
59
+ - 0
60
+ version: "0"
61
+ type: :runtime
62
62
  version_requirements: *id003
63
63
  - !ruby/object:Gem::Dependency
64
- name: rcov
64
+ name: activeresource
65
65
  prerelease: false
66
66
  requirement: &id004 !ruby/object:Gem::Requirement
67
67
  none: false
@@ -72,9 +72,37 @@ dependencies:
72
72
  segments:
73
73
  - 0
74
74
  version: "0"
75
- type: :development
75
+ type: :runtime
76
76
  version_requirements: *id004
77
- description: "Connect your business world with SalesKing. This gem gives ruby developers a jump-start for building SalesKing Business Apps. Under the hood it provides classes to handle oAuth, make RESTfull API requests and parses JSON Schema "
77
+ - !ruby/object:Gem::Dependency
78
+ name: rspec
79
+ prerelease: false
80
+ requirement: &id005 !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ hash: 3
86
+ segments:
87
+ - 0
88
+ version: "0"
89
+ type: :development
90
+ version_requirements: *id005
91
+ - !ruby/object:Gem::Dependency
92
+ name: rcov
93
+ prerelease: false
94
+ requirement: &id006 !ruby/object:Gem::Requirement
95
+ none: false
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ hash: 3
100
+ segments:
101
+ - 0
102
+ version: "0"
103
+ type: :development
104
+ version_requirements: *id006
105
+ description: "Connect your business world with SalesKing. This gem gives ruby developers a jump-start for building SalesKing Business Apps. It provides classes to handle oAuth, make RESTfull API requests and parses JSON Schema "
78
106
  email: gl@salesking.eu
79
107
  executables: []
80
108
 
@@ -87,22 +115,22 @@ files:
87
115
  - README.rdoc
88
116
  - Rakefile
89
117
  - VERSION
118
+ - ci/Gemfile
90
119
  - lib/sk_sdk.rb
91
120
  - lib/sk_sdk/README_Base.rdoc
92
- - lib/sk_sdk/ar_cli.rb
93
- - lib/sk_sdk/ar_cli/patches/ar2/base.rb
94
- - lib/sk_sdk/ar_cli/patches/ar2/validations.rb
95
- - lib/sk_sdk/ar_cli/patches/ar3/base.rb
96
- - lib/sk_sdk/ar_cli/patches/ar3/validations.rb
121
+ - lib/sk_sdk/ar_patches/ar2/base.rb
122
+ - lib/sk_sdk/ar_patches/ar2/validations.rb
123
+ - lib/sk_sdk/ar_patches/ar3/base.rb
124
+ - lib/sk_sdk/ar_patches/ar3/validations.rb
97
125
  - lib/sk_sdk/base.rb
98
126
  - lib/sk_sdk/oauth.rb
99
127
  - lib/sk_sdk/omni_auth/README.rdoc
100
128
  - lib/sk_sdk/omni_auth/salesking.rb
101
129
  - lib/sk_sdk/signed_request.rb
130
+ - lib/sk_sdk/sync.rb
102
131
  - sk_sdk.gemspec
103
132
  - spec/resources_spec_helper.rb
104
133
  - spec/settings.yml
105
- - spec/sk_sdk/ar_cli_spec.rb
106
134
  - spec/sk_sdk/base_spec.rb
107
135
  - spec/sk_sdk/oauth_spec.rb
108
136
  - spec/sk_sdk/resources/README.rdoc
@@ -111,6 +139,8 @@ files:
111
139
  - spec/sk_sdk/resources/invoice_spec.rb
112
140
  - spec/sk_sdk/resources/product_spec.rb
113
141
  - spec/sk_sdk/signed_request_spec.rb
142
+ - spec/sk_sdk/sync_field_spec.rb
143
+ - spec/sk_sdk/sync_spec.rb
114
144
  - spec/spec_helper.rb
115
145
  has_rdoc: true
116
146
  homepage: http://github.com/salesking/sk_sdk
data/lib/sk_sdk/ar_cli.rb DELETED
@@ -1,33 +0,0 @@
1
- require 'sk_sdk'
2
- require 'sk_sdk/base'
3
-
4
- module SK::SDK
5
- class ArCli
6
- # TODO deprecated
7
- # Create a class for a given name
8
- #
9
- # === Example
10
- #
11
- # SK::SDK::ArCli.make(:client)
12
- # c = Client.new
13
- #
14
- # SK::SDK::ArCli.make(:credit_note, SK::API)
15
- # i = SK::API::CreditNote.new
16
- #
17
- # === Parameter
18
- # name<String>:: lowercase, underscored name: line_item, client must be a
19
- # valid title of a json schema
20
- # obj_scope<Constant>:: class, module name under which to setup(namespace)
21
- # the new class. Default to Object, example: SK::API
22
- def self.make(name, obj_scope =nil)
23
- class_name = name.to_s.camelize
24
- # by default create class in Object scope
25
- obj_scope ||= Object
26
- # only define the class once
27
- raise "Constant #{class_name} already defined in scope of #{obj_scope}!" if obj_scope.const_defined?(class_name)
28
- # create a new class from given name:
29
- # :line_item => # class LineItem < ActiveResource::Base
30
- obj_scope.const_set( class_name, Class.new(SK::SDK::Base) )
31
- end
32
- end
33
- end
@@ -1,41 +0,0 @@
1
- require 'spec/spec_helper'
2
- require 'spec/resources_spec_helper'
3
-
4
- describe SK::SDK::ArCli, "make new class" do
5
-
6
- it "should create class" do
7
- c = Client.new
8
- c.first_name = 'herbert' # implicit setter
9
- c.first_name.should == 'herbert' # implicit getter
10
- c1 = Client.new
11
- end
12
-
13
- it "should have properties as attributes" do
14
- c = Client.new :some_field => ''
15
- c.attributes.should == {"some_field"=>""}
16
- end
17
-
18
- it "should create save method" do
19
- c = Client.new
20
- c.respond_to?(:save).should be_true
21
- end
22
-
23
- it "should have new_record?" do
24
- c = Client.new
25
- c.new_record?.should be_true
26
- end
27
-
28
- it "should raise error on second create" do
29
- lambda{
30
- SK::SDK::ArCli.make(:client)
31
- }.should raise_error(RuntimeError, "Constant Client already defined in scope of Object!")
32
- end
33
-
34
- it "should allow create a second class in different scope" do
35
- lambda{
36
- SK::SDK::ArCli.make(:client, SK::API)
37
- c = SK::API::Client.new
38
- c.id
39
- }.should_not raise_error(RuntimeError)
40
- end
41
- end