versacommerce_api 1.0.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/.gitignore +18 -0
- data/.ruby-version +1 -0
- data/CHANGELOG +2 -0
- data/CONTRIBUTORS +0 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +59 -0
- data/LICENSE.txt +22 -0
- data/README.md +200 -0
- data/RELEASING +16 -0
- data/Rakefile +1 -0
- data/bin/versacommerce +4 -0
- data/lib/active_resource/base_ext.rb +22 -0
- data/lib/active_resource/connection_ext.rb +31 -0
- data/lib/active_resource/detailed_log_subscriber.rb +19 -0
- data/lib/active_resource/disable_prefix_check.rb +8 -0
- data/lib/active_resource/json_errors.rb +21 -0
- data/lib/versacommerce_api.rb +21 -0
- data/lib/versacommerce_api/associatable.rb +25 -0
- data/lib/versacommerce_api/cli.rb +164 -0
- data/lib/versacommerce_api/countable.rb +9 -0
- data/lib/versacommerce_api/resources.rb +2 -0
- data/lib/versacommerce_api/resources/base.rb +42 -0
- data/lib/versacommerce_api/resources/billing_addres.rb +9 -0
- data/lib/versacommerce_api/resources/collection.rb +6 -0
- data/lib/versacommerce_api/resources/customer.rb +6 -0
- data/lib/versacommerce_api/resources/item.rb +9 -0
- data/lib/versacommerce_api/resources/link.rb +15 -0
- data/lib/versacommerce_api/resources/linklist.rb +11 -0
- data/lib/versacommerce_api/resources/order.rb +22 -0
- data/lib/versacommerce_api/resources/page.rb +6 -0
- data/lib/versacommerce_api/resources/payment.rb +6 -0
- data/lib/versacommerce_api/resources/product.rb +55 -0
- data/lib/versacommerce_api/resources/product_image.rb +67 -0
- data/lib/versacommerce_api/resources/property.rb +6 -0
- data/lib/versacommerce_api/resources/shipment.rb +11 -0
- data/lib/versacommerce_api/resources/shipping_addres.rb +9 -0
- data/lib/versacommerce_api/resources/shop.rb +10 -0
- data/lib/versacommerce_api/resources/variant.rb +7 -0
- data/lib/versacommerce_api/session.rb +88 -0
- data/lib/versacommerce_api/version.rb +3 -0
- data/spec/resources/order_spec.rb +71 -0
- data/spec/resources/product_spec.rb +73 -0
- data/spec/spec_helper.rb +18 -0
- data/versacommerce_api.gemspec +37 -0
- 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,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,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,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
|