versacommerce_api 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/.gitignore +18 -0
  2. data/.ruby-version +1 -0
  3. data/CHANGELOG +2 -0
  4. data/CONTRIBUTORS +0 -0
  5. data/Gemfile +4 -0
  6. data/Gemfile.lock +59 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +200 -0
  9. data/RELEASING +16 -0
  10. data/Rakefile +1 -0
  11. data/bin/versacommerce +4 -0
  12. data/lib/active_resource/base_ext.rb +22 -0
  13. data/lib/active_resource/connection_ext.rb +31 -0
  14. data/lib/active_resource/detailed_log_subscriber.rb +19 -0
  15. data/lib/active_resource/disable_prefix_check.rb +8 -0
  16. data/lib/active_resource/json_errors.rb +21 -0
  17. data/lib/versacommerce_api.rb +21 -0
  18. data/lib/versacommerce_api/associatable.rb +25 -0
  19. data/lib/versacommerce_api/cli.rb +164 -0
  20. data/lib/versacommerce_api/countable.rb +9 -0
  21. data/lib/versacommerce_api/resources.rb +2 -0
  22. data/lib/versacommerce_api/resources/base.rb +42 -0
  23. data/lib/versacommerce_api/resources/billing_addres.rb +9 -0
  24. data/lib/versacommerce_api/resources/collection.rb +6 -0
  25. data/lib/versacommerce_api/resources/customer.rb +6 -0
  26. data/lib/versacommerce_api/resources/item.rb +9 -0
  27. data/lib/versacommerce_api/resources/link.rb +15 -0
  28. data/lib/versacommerce_api/resources/linklist.rb +11 -0
  29. data/lib/versacommerce_api/resources/order.rb +22 -0
  30. data/lib/versacommerce_api/resources/page.rb +6 -0
  31. data/lib/versacommerce_api/resources/payment.rb +6 -0
  32. data/lib/versacommerce_api/resources/product.rb +55 -0
  33. data/lib/versacommerce_api/resources/product_image.rb +67 -0
  34. data/lib/versacommerce_api/resources/property.rb +6 -0
  35. data/lib/versacommerce_api/resources/shipment.rb +11 -0
  36. data/lib/versacommerce_api/resources/shipping_addres.rb +9 -0
  37. data/lib/versacommerce_api/resources/shop.rb +10 -0
  38. data/lib/versacommerce_api/resources/variant.rb +7 -0
  39. data/lib/versacommerce_api/session.rb +88 -0
  40. data/lib/versacommerce_api/version.rb +3 -0
  41. data/spec/resources/order_spec.rb +71 -0
  42. data/spec/resources/product_spec.rb +73 -0
  43. data/spec/spec_helper.rb +18 -0
  44. data/versacommerce_api.gemspec +37 -0
  45. metadata +242 -0
@@ -0,0 +1,21 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ require 'active_resource'
4
+ require 'active_support/core_ext/class/attribute_accessors'
5
+ require 'digest/md5'
6
+ require 'base64'
7
+ require 'active_resource/connection_ext'
8
+ require 'active_resource/detailed_log_subscriber'
9
+ require 'active_resource/json_errors'
10
+ require 'active_resource/disable_prefix_check'
11
+ require 'active_resource/base_ext'
12
+
13
+ module VersacommerceAPI
14
+ # Your code goes here...
15
+ end
16
+
17
+ require "versacommerce_api/associatable"
18
+ require "versacommerce_api/version"
19
+ require 'versacommerce_api/countable'
20
+ require 'versacommerce_api/resources'
21
+ require 'versacommerce_api/session'
@@ -0,0 +1,25 @@
1
+ module VersacommerceAPI
2
+
3
+ module Associatable
4
+ def associated_resource association, pluralize=true
5
+ association = association.pluralize if pluralize
6
+
7
+ if self.new? or attributes[association]
8
+ attributes[association]
9
+ else
10
+ # dont create traffic if we know already there are no associated records...
11
+ return [] if self.respond_to?("#{association}_count") && self.send("#{association}_count") == 0 && pluralize
12
+ klass = "VersacommerceAPI::#{association.classify}".constantize
13
+ # ensure we don´t have the association already included
14
+ attributes[association] ||= begin
15
+ resource_name = self.class.name.split("::").last.tableize
16
+ klass.prefix = "/api/#{resource_name}/#{self.id}/"
17
+ pluralize ? klass.find(:all) : klass.find(:one)
18
+ end
19
+ klass.root!
20
+ attributes[association]
21
+ end
22
+ end
23
+ end
24
+
25
+ end
@@ -0,0 +1,164 @@
1
+ require 'thor'
2
+ require 'abbrev'
3
+
4
+ module VersacommerceAPI
5
+ class Cli < Thor
6
+ include Thor::Actions
7
+
8
+ class ConfigFileError < StandardError
9
+ end
10
+
11
+ desc "list", "List all saved CONNECTIONs"
12
+ def list
13
+ available_connections.each do |c|
14
+ prefix = default?(c) ? " * " : " "
15
+ puts prefix + c
16
+ end
17
+ end
18
+
19
+ desc "add [CONNECTION]", "Create a config file for a connection named CONNECTION"
20
+ def add(connection)
21
+ file = config_file(connection)
22
+ if File.exist?(file)
23
+ raise ConfigFileError, "There is already a config file at #{file}"
24
+ else
25
+ config = {'protocol' => 'http'}
26
+ config['domain'] = ask("Domain (leave blank for #{connection}.versacommerce.de):")
27
+ config['domain'] = "#{connection}.versacommerce.de" if config['domain'].blank?
28
+ config['domain'] = "#{config['domain']}.versacommerce.de" unless config['domain'].match(/[.:]/)
29
+ puts "\nopen http://#{config['domain']}/admin/settings/apps in your browser to get API credentials\n"
30
+ config['api_key'] = ask("API key :")
31
+ config['password'] = ask("Password:")
32
+ create_file(file, config.to_yaml)
33
+ end
34
+ if available_connections.one?
35
+ default(connection)
36
+ end
37
+ end
38
+
39
+ desc "remove [CONNECTION]", "Remove the config file for CONNECTION"
40
+ def remove(connection)
41
+ file = config_file(connection)
42
+ if File.exist?(file)
43
+ remove_file(default_symlink) if default?(connection)
44
+ remove_file(file)
45
+ else
46
+ config_file_not_found_error(file)
47
+ end
48
+ end
49
+
50
+ desc "edit [CONNECTION]", "Open the config file for CONNECTION with your default editor"
51
+ def edit(connection=nil)
52
+ file = config_file(connection)
53
+ if File.exist?(file)
54
+ if ENV['EDITOR'].present?
55
+ system(ENV['EDITOR'], file)
56
+ else
57
+ puts "Please set an editor in the EDITOR environment variable"
58
+ end
59
+ else
60
+ config_file_not_found_error(file)
61
+ end
62
+ end
63
+
64
+ desc "show [CONNECTION]", "Show the location and contents of the CONNECTION's config file"
65
+ def show(connection=nil)
66
+ connection ||= default_connection
67
+ file = config_file(connection)
68
+ if File.exist?(file)
69
+ puts file
70
+ puts `cat #{file}`
71
+ else
72
+ config_file_not_found_error(file)
73
+ end
74
+ end
75
+
76
+ desc "default [CONNECTION]", "Show or set the default connection"
77
+ def default(connection=nil)
78
+ if connection
79
+ target = config_file(connection)
80
+ if File.exist?(target)
81
+ remove_file(default_symlink)
82
+ `ln -s #{target} #{default_symlink}`
83
+ else
84
+ config_file_not_found_error(target)
85
+ end
86
+ end
87
+ if File.exist?(default_symlink)
88
+ puts "Default connection is #{default_connection}"
89
+ else
90
+ puts "There is no default connection set"
91
+ end
92
+ end
93
+
94
+ desc "console [CONNECTION]", "Startup an API console for CONNECTION"
95
+ def console(connection=nil)
96
+ file = config_file(connection)
97
+
98
+ config = YAML.load(File.read(file))
99
+ puts ""
100
+ puts "--> Starting interactive API console for #{config['domain']} - #{file}"
101
+ puts ""
102
+ VersacommerceAPI::Base.site = site_from_config(config)
103
+
104
+ require 'irb'
105
+ require 'irb/completion'
106
+ ARGV.clear
107
+ IRB.start
108
+ end
109
+
110
+ tasks.keys.abbrev.each do |shortcut, command|
111
+ map shortcut => command.to_sym
112
+ end
113
+
114
+ private
115
+
116
+ def shop_config_dir
117
+ @shop_config_dir ||= File.join(ENV['HOME'], '.versacommerce', 'shops')
118
+ end
119
+
120
+ def default_symlink
121
+ @default_symlink ||= File.join(shop_config_dir, 'default')
122
+ end
123
+
124
+ def config_file(connection)
125
+ if connection
126
+ File.join(shop_config_dir, "#{connection}.yml")
127
+ else
128
+ default_symlink
129
+ end
130
+ end
131
+
132
+ def site_from_config(config)
133
+ protocol = config['protocol'] || 'https'
134
+ api_key = config['api_key']
135
+ password = config['password']
136
+ domain = config['domain']
137
+
138
+ VersacommerceAPI::Base.site = "#{protocol}://#{api_key}:#{password}@#{domain}/api"
139
+ end
140
+
141
+ def available_connections
142
+ @available_connections ||= begin
143
+ pattern = File.join(shop_config_dir, "*.yml")
144
+ Dir.glob(pattern).map { |f| File.basename(f, ".yml") }
145
+ end
146
+ end
147
+
148
+ def default_connection_target
149
+ @default_connection_target ||= File.readlink(default_symlink)
150
+ end
151
+
152
+ def default_connection
153
+ @default_connection ||= File.basename(default_connection_target, ".yml")
154
+ end
155
+
156
+ def default?(connection)
157
+ default_connection == connection
158
+ end
159
+
160
+ def config_file_not_found_error(filename)
161
+ raise ConfigFileError, "Could not find config file at #{filename}"
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,9 @@
1
+ module VersacommerceAPI
2
+
3
+ module Countable
4
+ def count(options = {})
5
+ Integer(get(:count, options))
6
+ end
7
+ end
8
+
9
+ end
@@ -0,0 +1,2 @@
1
+ require 'versacommerce_api/resources/base'
2
+ Dir.glob "#{File.dirname(__FILE__)}/resources/*", &method(:require)
@@ -0,0 +1,42 @@
1
+ module VersacommerceAPI
2
+
3
+ class Base < ActiveResource::Base
4
+ extend Countable
5
+ self.format = :xml
6
+ self.headers['User-Agent'] = ["VersacommerceAPI/#{VersacommerceAPI::VERSION}",
7
+ "ActiveResource/#{ActiveResource::VERSION::STRING}",
8
+ "Ruby/#{RUBY_VERSION}"].join(' ')
9
+
10
+ def self.all
11
+ self.find(:all)
12
+ end
13
+
14
+ def self.root!
15
+ self.prefix = "/"
16
+ end
17
+
18
+ class << self
19
+ def headers
20
+ if defined?(@headers)
21
+ @headers
22
+ elsif superclass != Object && superclass.headers
23
+ superclass.headers
24
+ else
25
+ @headers ||= {}
26
+ end
27
+ end
28
+
29
+ def activate_session(session)
30
+ self.site = session.site
31
+ self.headers.merge!('X-Versacommerce-API-Token' => session.token)
32
+ end
33
+
34
+ def clear_session
35
+ self.site = nil
36
+ self.headers.delete('X-Versacommerce-API-Token')
37
+ end
38
+ end
39
+
40
+ end
41
+
42
+ end
@@ -0,0 +1,9 @@
1
+ module VersacommerceAPI
2
+
3
+ class BillingAddres < Base
4
+ def fullname
5
+ [firstname, lastname].delete_if(&:blank?).compact * " "
6
+ end
7
+ end
8
+
9
+ end
@@ -0,0 +1,6 @@
1
+ module VersacommerceAPI
2
+
3
+ class Collection < Base
4
+ end
5
+
6
+ end
@@ -0,0 +1,6 @@
1
+ module VersacommerceAPI
2
+
3
+ class Customer < Base
4
+ end
5
+
6
+ end
@@ -0,0 +1,9 @@
1
+ module VersacommerceAPI
2
+
3
+ class Item < Base
4
+ def product
5
+ Product.find product_id
6
+ end
7
+ end
8
+
9
+ end
@@ -0,0 +1,15 @@
1
+ module VersacommerceAPI
2
+
3
+ class Link < Base
4
+
5
+ def linked
6
+ resource.find(self.linkable_id) if self.linkable_id && resource
7
+ end
8
+
9
+ def resource
10
+ "VersaCommerceShopApi::#{self.linkable_type}".constantize rescue nil
11
+ end
12
+
13
+ end
14
+
15
+ end
@@ -0,0 +1,11 @@
1
+ module VersacommerceAPI
2
+
3
+ class Linklist < Base
4
+ include Associatable
5
+
6
+ def links
7
+ associated_resource "link"
8
+ end
9
+ end
10
+
11
+ end
@@ -0,0 +1,22 @@
1
+ module VersacommerceAPI
2
+
3
+ # An order comes by default with three associations: shipping_address, billing_address and customer.
4
+ # because we already know the amount of data. if you want to include order items use the :include option like:
5
+ # VersaCommerceShopApi::Order.find(:all, :params => {:include => 'items', :limit => 10})
6
+ class Order < Base
7
+ include Associatable
8
+
9
+ def payments
10
+ associated_resource "payment"
11
+ end
12
+
13
+ def shipping_address
14
+ associated_resource "shipping_address", false
15
+ end
16
+
17
+ def billing_address
18
+ associated_resource "billing_address", false
19
+ end
20
+ end
21
+
22
+ end
@@ -0,0 +1,6 @@
1
+ module VersacommerceAPI
2
+
3
+ class Page < Base
4
+ end
5
+
6
+ end
@@ -0,0 +1,6 @@
1
+ module VersacommerceAPI
2
+
3
+ class Payment < Base
4
+ end
5
+
6
+ end
@@ -0,0 +1,55 @@
1
+ module VersacommerceAPI
2
+
3
+ class Product < Base
4
+ include Associatable
5
+
6
+ # compute the price range
7
+ def price_range
8
+ prices = variants.collect(&:price)
9
+ format = "%0.2f"
10
+ if prices.min != prices.max
11
+ "#{format % prices.min} - #{format % prices.max}"
12
+ else
13
+ format % prices.min
14
+ end
15
+ end
16
+
17
+ def available
18
+ amount_available(0) && active
19
+ end
20
+
21
+ def amount_available(amount)
22
+ if considers_stock
23
+ stock >= amount
24
+ else
25
+ true
26
+ end
27
+ end
28
+
29
+ def is_variant
30
+ !product_id.nil?
31
+ end
32
+
33
+ def properties
34
+ associated_resource "property"
35
+ end
36
+
37
+ def product_images
38
+ associated_resource "product_image"
39
+ end
40
+
41
+ def variants
42
+ associated_resource "variant"
43
+ end
44
+
45
+ def tags
46
+ return [] if self.respond_to?("tag_list") && self.send("tag_list").blank?
47
+ tag_list.split(",").map(&:strip)
48
+ end
49
+
50
+ def featured_image
51
+ ProductImage.new(:src => featured_image_url)
52
+ end
53
+ end
54
+
55
+ end
@@ -0,0 +1,67 @@
1
+ module VersacommerceAPI
2
+
3
+ class ProductImage < Base
4
+
5
+ def initialize(attributes)
6
+ super
7
+ if self.attributes['file']
8
+ file = self.attributes['file']
9
+ data = file.read
10
+ type, suffix = file.content_type.split("/")
11
+ raise ArgumentError.new("file must be an image") unless ((type == "image") && %w(jpeg jpg gif png).include?(suffix))
12
+ filename = "upload_file.#{suffix}"
13
+ upload_image(data, filename)
14
+ self.attributes.delete 'file'
15
+ end
16
+ end
17
+
18
+ def pico
19
+ generate_resized_url(original, :resize, '16x16')
20
+ end
21
+
22
+ def icon
23
+ generate_resized_url(original, :resize, '32x32')
24
+ end
25
+
26
+ def thumb
27
+ generate_resized_url(original, :resize, '50x50')
28
+ end
29
+
30
+ def small
31
+ generate_resized_url(original, :resize, '100x100')
32
+ end
33
+
34
+ def medium
35
+ generate_resized_url(original, :resize, '240x240')
36
+ end
37
+
38
+ def large
39
+ generate_resized_url(original, :resize, '480x480')
40
+ end
41
+
42
+ def xlarge
43
+ generate_resized_url(original, :resize, '960x960')
44
+ end
45
+
46
+ def standard
47
+ generate_resized_url(original, :resize, '1024x1024')
48
+ end
49
+
50
+ def original
51
+ "http://images.versacommerce.net/++/#{src.gsub("http://", "")}"
52
+ end
53
+
54
+ def upload_image(data, filename = nil)
55
+ attributes['image_data'] = Base64.encode64(data)
56
+ attributes['filename'] = filename unless filename.nil?
57
+ end
58
+
59
+ private
60
+
61
+ def generate_resized_url(url, command, value)
62
+ refit_url = url.gsub("\/++\/", "\/#{command}=#{value}\/++\/")
63
+ end
64
+
65
+ end
66
+
67
+ end