solidus_feeds 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +41 -0
  3. data/.gem_release.yml +5 -0
  4. data/.github/stale.yml +17 -0
  5. data/.github_changelog_generator +2 -0
  6. data/.gitignore +20 -0
  7. data/.rspec +2 -0
  8. data/.rubocop.yml +12 -0
  9. data/Gemfile +33 -0
  10. data/LICENSE +26 -0
  11. data/README.md +246 -0
  12. data/Rakefile +6 -0
  13. data/app/assets/javascripts/spree/backend/solidus_feeds.js +2 -0
  14. data/app/assets/javascripts/spree/frontend/solidus_feeds.js +2 -0
  15. data/app/assets/stylesheets/spree/backend/solidus_feeds.css +4 -0
  16. data/app/assets/stylesheets/spree/frontend/solidus_feeds.css +4 -0
  17. data/bin/console +17 -0
  18. data/bin/rails +7 -0
  19. data/bin/rails-engine +13 -0
  20. data/bin/rails-sandbox +16 -0
  21. data/bin/rake +7 -0
  22. data/bin/sandbox +86 -0
  23. data/bin/setup +8 -0
  24. data/config/locales/en.yml +5 -0
  25. data/config/routes.rb +5 -0
  26. data/lib/generators/solidus_feeds/install/install_generator.rb +37 -0
  27. data/lib/generators/solidus_feeds/install/templates/initializer.rb +15 -0
  28. data/lib/solidus_feeds.rb +13 -0
  29. data/lib/solidus_feeds/configuration.rb +59 -0
  30. data/lib/solidus_feeds/engine.rb +19 -0
  31. data/lib/solidus_feeds/factories.rb +4 -0
  32. data/lib/solidus_feeds/feed.rb +21 -0
  33. data/lib/solidus_feeds/generators/google_merchant.rb +106 -0
  34. data/lib/solidus_feeds/publishers/s3.rb +30 -0
  35. data/lib/solidus_feeds/publishers/static_file.rb +17 -0
  36. data/lib/solidus_feeds/testing_support/factories.rb +4 -0
  37. data/lib/solidus_feeds/version.rb +5 -0
  38. data/solidus_feeds.gemspec +36 -0
  39. data/spec/lib/solidus_feeds/generators/google_merchant_spec.rb +132 -0
  40. data/spec/lib/solidus_feeds/publishers/s3_spec.rb +37 -0
  41. data/spec/lib/solidus_feeds/publishers/static_file_spec.rb +31 -0
  42. data/spec/lib/solidus_feeds/solidus_feeds_spec.rb +23 -0
  43. data/spec/solidus_feeds_spec.rb +29 -0
  44. data/spec/spec_helper.rb +31 -0
  45. metadata +157 -0
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aws-sdk-s3'
4
+
5
+ module SolidusFeeds
6
+ module Publishers
7
+ class S3
8
+ attr_reader :object_key, :bucket, :resource
9
+
10
+ NoContentError = Class.new(StandardError)
11
+
12
+ def initialize(object_key:, bucket:, client: Aws::S3::Client.new)
13
+ @object_key = object_key
14
+ @bucket = bucket
15
+ @resource = Aws::S3::Resource.new(client: client)
16
+ end
17
+
18
+ def call
19
+ Tempfile.create(object_key) do |io|
20
+ yield io
21
+ io.rewind
22
+ raise NoContentError, "no content was generated" if io.eof?
23
+
24
+ object = resource.bucket(bucket).object(object_key)
25
+ object.put(body: io, acl: 'public-read')
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusFeeds
4
+ module Publishers
5
+ class StaticFile
6
+ attr_reader :path
7
+
8
+ def initialize(path:)
9
+ @path = path
10
+ end
11
+
12
+ def call(&block)
13
+ File.open(path, 'w', &block)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusFeeds
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/solidus_feeds/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'solidus_feeds'
7
+ spec.version = SolidusFeeds::VERSION
8
+ spec.authors = ['Elia Schito', 'Nicolò Rebughini']
9
+ spec.email = 'contact@solidus.io'
10
+
11
+ spec.summary = 'A framework for producing and publishing feeds on Solidus.'
12
+ spec.homepage = 'https://github.com/solidusio-contrib/solidus_feeds#readme'
13
+ spec.license = 'BSD-3-Clause'
14
+
15
+ spec.metadata['homepage_uri'] = spec.homepage
16
+ spec.metadata['source_code_uri'] = 'https://github.com/solidusio-contrib/solidus_feeds'
17
+ spec.metadata['changelog_uri'] = 'https://github.com/solidusio-contrib/solidus_feeds/releases'
18
+
19
+ spec.required_ruby_version = Gem::Requirement.new('~> 2.5')
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ files = Dir.chdir(__dir__) { `git ls-files -z`.split("\x0") }
24
+
25
+ spec.files = files.grep_v(%r{^(test|spec|features)/})
26
+ spec.test_files = files.grep(%r{^(test|spec|features)/})
27
+ spec.bindir = "exe"
28
+ spec.executables = files.grep(%r{^exe/}) { |f| File.basename(f) }
29
+ spec.require_paths = ["lib"]
30
+
31
+ spec.add_dependency 'aws-sdk-s3', '~> 1.83'
32
+ spec.add_dependency 'solidus_core', ['>= 2.0.0', '< 3']
33
+ spec.add_dependency 'solidus_support', '~> 0.5'
34
+
35
+ spec.add_development_dependency 'solidus_dev_support', '~> 2.3'
36
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'stringio'
5
+
6
+ RSpec.describe SolidusFeeds::Generators::GoogleMerchant do
7
+ subject(:generator) { described_class.new(products, host: 'https://example.com') }
8
+
9
+ let(:products) do
10
+ Spree::Product.where(id: [
11
+ create(:product, id: 123, slug: 'pro1', name: 'A product', price: 123.45, description: 'product description', sku: 'PRD1'), # rubocop:disable Layout/LineLength
12
+ create(:product, id: 456, slug: 'pro2', name: 'Another product', price: 678.90, description: 'another product description', sku: 'PRD2'), # rubocop:disable Layout/LineLength
13
+ ]).order(:id)
14
+ end
15
+ let(:product) { products.first }
16
+ let(:store) { instance_double(Spree::Store, name: "The Best Store", url: "example.com") }
17
+
18
+ before { allow(Spree::Store).to receive(:default).and_return(store) }
19
+
20
+ before(:each, with_images: true) do
21
+ allow(ActionController::Base).to receive(:asset_host).and_return('https://assets.example.com')
22
+ Spree::Image.create! viewable: products.first.master, attachment_file_name: 'foo.png', id: 234
23
+ end
24
+
25
+ describe '#call', :with_images do
26
+ describe 'generated XML' do
27
+ def expect_xml_content(xpath)
28
+ expect(xml.xpath(xpath, 'g' => "http://base.google.com/ns/1.0").text)
29
+ end
30
+
31
+ let(:xml) do
32
+ io = StringIO.new
33
+ generator.call(io)
34
+ Nokogiri::XML(io.string)
35
+ end
36
+
37
+ it 'includes the tags with the expected values' do
38
+ aggregate_failures do
39
+ expect_xml_content('//channel/title').to eq('The Best Store')
40
+ expect_xml_content('//channel/link').to eq('https://example.com')
41
+ expect_xml_content('//channel/description').to eq('Find out about new products on https://example.com first!')
42
+ expect_xml_content('//channel/language').to eq('en-us')
43
+ expect_xml_content('//channel/item[1]/g:id').to eq('123')
44
+ expect_xml_content('//channel/item[1]/g:title').to eq('A product')
45
+ expect_xml_content('//channel/item[1]/g:description').to eq('product description')
46
+ expect_xml_content('//channel/item[1]/g:link').to eq('https://example.com/products/pro1')
47
+ expect_xml_content('//channel/item[1]/g:image_link').to eq("https://assets.example.com/spree/products/234/large/foo.png") # rubocop:disable Layout/LineLength
48
+ expect_xml_content('//channel/item[1]/g:condition').to eq('new')
49
+ expect_xml_content('//channel/item[1]/g:price').to eq('123.45 USD')
50
+ expect_xml_content('//channel/item[1]/g:availability').to eq('out of stock')
51
+ expect_xml_content('//channel/item[1]/g:brand').to eq('The Best Store')
52
+ expect_xml_content('//channel/item[1]/g:mpn').to eq('PRD1')
53
+ expect_xml_content('//channel/item[1]/g:google_product_category').to eq('')
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ specify '#id' do
60
+ product = instance_double(Spree::Product, id: 789)
61
+
62
+ expect(generator.id(product)).to eq(789)
63
+ end
64
+
65
+ specify '#title' do
66
+ product = instance_double(Spree::Product, name: "Foo Bar")
67
+
68
+ expect(generator.title(product)).to eq("Foo Bar")
69
+ end
70
+
71
+ specify '#description' do
72
+ product = instance_double(Spree::Product, description: "Foo Bar")
73
+
74
+ expect(generator.description(product)).to eq("Foo Bar")
75
+ end
76
+
77
+ specify '#link' do
78
+ product = instance_double(Spree::Product, to_param: "123-my-product")
79
+
80
+ expect(generator.link(product)).to eq("https://example.com/products/123-my-product")
81
+ end
82
+
83
+ specify '#image_link', :with_images do
84
+ expect(generator.image_link(products.first)).to eq("https://assets.example.com/spree/products/234/large/foo.png")
85
+ expect(generator.image_link(products.second)).to eq(nil)
86
+ end
87
+
88
+ specify '#condition' do
89
+ products.first.set_property("condition", "foo-bar")
90
+
91
+ expect(generator.condition(product)).to eq("foo-bar")
92
+ end
93
+
94
+ specify '#price' do
95
+ product = instance_double(Spree::Product, price: 321.45);
96
+
97
+ expect(generator.price(product)).to eq("321.45 USD")
98
+ end
99
+
100
+ describe '#availability' do
101
+ it 'is "in stock" when available' do
102
+ allow(product.master).to receive(:in_stock?).and_return(true)
103
+
104
+ expect(generator.availability(product)).to eq("in stock")
105
+ end
106
+
107
+ it 'is "out of stock" when unavailable' do
108
+ allow(product.master).to receive(:in_stock?).and_return(false)
109
+
110
+ expect(generator.availability(product)).to eq("out of stock")
111
+ end
112
+ end
113
+
114
+ specify '#brand' do
115
+ product.set_property("brand", "foo-bar")
116
+
117
+ expect(generator.brand(product)).to eq("foo-bar")
118
+ expect(generator.brand(products.second)).to eq("The Best Store")
119
+ end
120
+
121
+ specify '#mpn' do
122
+ allow(product.master).to receive(:sku).and_return("FOOO_123_SKU")
123
+
124
+ expect(generator.mpn(product)).to eq("FOOO_123_SKU")
125
+ end
126
+
127
+ specify '#google_product_category_id' do
128
+ product.set_property("google_product_category_id", "123456")
129
+
130
+ expect(generator.google_product_category_id(product)).to eq("123456")
131
+ end
132
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'csv'
4
+
5
+ RSpec.describe SolidusFeeds::Publishers::S3 do
6
+ let(:io) { StringIO.new }
7
+ let(:client) { Aws::S3::Client.new(stub_responses: true) }
8
+ let(:generator) {
9
+ ->(io) {
10
+ csv = CSV.new(io)
11
+ csv << ["some", "data"]
12
+ csv << ["another", "line"]
13
+ }
14
+ }
15
+
16
+ describe '#call' do
17
+ it 'correctly uploads the generated content to S3' do
18
+ s3_publisher = described_class.new(object_key: 'my_feed.xml', bucket: 'dummy_bucket', client: client)
19
+
20
+ response = s3_publisher.call do |io|
21
+ generator.call(io)
22
+ end
23
+
24
+ expect(response).to be_instance_of(Aws::S3::Types::PutObjectOutput)
25
+ end
26
+
27
+ it 'raises an error if the generator does not generate anything' do
28
+ s3_publisher = described_class.new(object_key: 'my_feed.xml', bucket: 'dummy_bucket', client: client)
29
+
30
+ expect {
31
+ s3_publisher.call do |io|
32
+ io << ""
33
+ end
34
+ }.to raise_error SolidusFeeds::Publishers::S3::NoContentError
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'csv'
4
+
5
+ RSpec.describe SolidusFeeds::Publishers::StaticFile do
6
+ let(:filename) { 'my_feed.csv' }
7
+ let(:io) { StringIO.new }
8
+ let(:generator) {
9
+ ->(io) {
10
+ csv = CSV.new(io)
11
+ csv << ["some", "data"]
12
+ csv << ["another", "line"]
13
+ }
14
+ }
15
+
16
+ describe '#call' do
17
+ it 'saves the generated content to the specified file' do
18
+ buffer = StringIO.new
19
+ allow(File).to receive(:open).with(filename, 'w').and_yield(buffer)
20
+ static_file_publisher = described_class.new(path: filename)
21
+
22
+ static_file_publisher.call do |io|
23
+ generator.call(io)
24
+ end
25
+
26
+ expect(buffer.string).to eq(
27
+ "some,data\nanother,line\n"
28
+ )
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe SolidusFeeds do
4
+ let(:store) { instance_double(Spree::Store, name: "The Best Store", url: "example.com") }
5
+
6
+ before { allow(Spree::Store).to receive(:default).and_return(store) }
7
+
8
+ specify '.title' do
9
+ expect(described_class.title).to eq("The Best Store")
10
+ end
11
+
12
+ specify '.link' do
13
+ expect(described_class.link).to eq("https://example.com")
14
+ end
15
+
16
+ specify '.description' do
17
+ expect(described_class.description).to eq("Find out about new products on https://example.com first!")
18
+ end
19
+
20
+ specify '.language' do
21
+ expect(described_class.language).to eq("en-us")
22
+ end
23
+ end
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+ require 'stringio'
3
+ require 'csv'
4
+
5
+ RSpec.describe SolidusFeeds do
6
+ before { described_class.reset_config! }
7
+
8
+ let(:io) { StringIO.new }
9
+ let(:string_io_publisher) { ->(&block) { block.call(io) } }
10
+ let(:csv_generator) {
11
+ ->(io) {
12
+ csv = CSV.new(io)
13
+ csv << ["some", "data"]
14
+ csv << ["another", "line"]
15
+ }
16
+ }
17
+
18
+ it 'allows to register, generate, and publish feeds' do
19
+ described_class.configure do |config|
20
+ config.register :foo do |feed|
21
+ feed.publisher = string_io_publisher
22
+ feed.generator = csv_generator
23
+ end
24
+ end
25
+
26
+ described_class.config.find(:foo).publish
27
+ expect(io.string).to eq("some,data\nanother,line\n")
28
+ end
29
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Configure Rails Environment
4
+ ENV['RAILS_ENV'] = 'test'
5
+
6
+ # Run Coverage report
7
+ require 'solidus_dev_support/rspec/coverage'
8
+
9
+ # Create the dummy app if it's still missing.
10
+ dummy_env = "#{__dir__}/dummy/config/environment.rb"
11
+ system 'bin/rake extension:test_app' unless File.exist? dummy_env
12
+ require dummy_env
13
+
14
+ # Requires factories and other useful helpers defined in spree_core.
15
+ require 'solidus_dev_support/rspec/feature_helper'
16
+
17
+ # Requires supporting ruby files with custom matchers and macros, etc,
18
+ # in spec/support/ and its subdirectories.
19
+ Dir["#{__dir__}/support/**/*.rb"].sort.each { |f| require f }
20
+
21
+ # Requires factories defined in lib/solidus_feeds/testing_support/factories.rb
22
+ SolidusDevSupport::TestingSupport::Factories.load_for(SolidusFeeds::Engine)
23
+
24
+ RSpec.configure do |config|
25
+ config.infer_spec_type_from_file_location!
26
+ config.use_transactional_fixtures = false
27
+
28
+ if Spree.solidus_gem_version < Gem::Version.new('2.11')
29
+ config.extend Spree::TestingSupport::AuthorizationHelpers::Request, type: :system
30
+ end
31
+ end
metadata ADDED
@@ -0,0 +1,157 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: solidus_feeds
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Elia Schito
8
+ - Nicolò Rebughini
9
+ autorequire:
10
+ bindir: exe
11
+ cert_chain: []
12
+ date: 2021-01-29 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: aws-sdk-s3
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '1.83'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '1.83'
28
+ - !ruby/object:Gem::Dependency
29
+ name: solidus_core
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: 2.0.0
35
+ - - "<"
36
+ - !ruby/object:Gem::Version
37
+ version: '3'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: 2.0.0
45
+ - - "<"
46
+ - !ruby/object:Gem::Version
47
+ version: '3'
48
+ - !ruby/object:Gem::Dependency
49
+ name: solidus_support
50
+ requirement: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.5'
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.5'
62
+ - !ruby/object:Gem::Dependency
63
+ name: solidus_dev_support
64
+ requirement: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.3'
69
+ type: :development
70
+ prerelease: false
71
+ version_requirements: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2.3'
76
+ description:
77
+ email: contact@solidus.io
78
+ executables: []
79
+ extensions: []
80
+ extra_rdoc_files: []
81
+ files:
82
+ - ".circleci/config.yml"
83
+ - ".gem_release.yml"
84
+ - ".github/stale.yml"
85
+ - ".github_changelog_generator"
86
+ - ".gitignore"
87
+ - ".rspec"
88
+ - ".rubocop.yml"
89
+ - Gemfile
90
+ - LICENSE
91
+ - README.md
92
+ - Rakefile
93
+ - app/assets/javascripts/spree/backend/solidus_feeds.js
94
+ - app/assets/javascripts/spree/frontend/solidus_feeds.js
95
+ - app/assets/stylesheets/spree/backend/solidus_feeds.css
96
+ - app/assets/stylesheets/spree/frontend/solidus_feeds.css
97
+ - bin/console
98
+ - bin/rails
99
+ - bin/rails-engine
100
+ - bin/rails-sandbox
101
+ - bin/rake
102
+ - bin/sandbox
103
+ - bin/setup
104
+ - config/locales/en.yml
105
+ - config/routes.rb
106
+ - lib/generators/solidus_feeds/install/install_generator.rb
107
+ - lib/generators/solidus_feeds/install/templates/initializer.rb
108
+ - lib/solidus_feeds.rb
109
+ - lib/solidus_feeds/configuration.rb
110
+ - lib/solidus_feeds/engine.rb
111
+ - lib/solidus_feeds/factories.rb
112
+ - lib/solidus_feeds/feed.rb
113
+ - lib/solidus_feeds/generators/google_merchant.rb
114
+ - lib/solidus_feeds/publishers/s3.rb
115
+ - lib/solidus_feeds/publishers/static_file.rb
116
+ - lib/solidus_feeds/testing_support/factories.rb
117
+ - lib/solidus_feeds/version.rb
118
+ - solidus_feeds.gemspec
119
+ - spec/lib/solidus_feeds/generators/google_merchant_spec.rb
120
+ - spec/lib/solidus_feeds/publishers/s3_spec.rb
121
+ - spec/lib/solidus_feeds/publishers/static_file_spec.rb
122
+ - spec/lib/solidus_feeds/solidus_feeds_spec.rb
123
+ - spec/solidus_feeds_spec.rb
124
+ - spec/spec_helper.rb
125
+ homepage: https://github.com/solidusio-contrib/solidus_feeds#readme
126
+ licenses:
127
+ - BSD-3-Clause
128
+ metadata:
129
+ homepage_uri: https://github.com/solidusio-contrib/solidus_feeds#readme
130
+ source_code_uri: https://github.com/solidusio-contrib/solidus_feeds
131
+ changelog_uri: https://github.com/solidusio-contrib/solidus_feeds/releases
132
+ post_install_message:
133
+ rdoc_options: []
134
+ require_paths:
135
+ - lib
136
+ required_ruby_version: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - "~>"
139
+ - !ruby/object:Gem::Version
140
+ version: '2.5'
141
+ required_rubygems_version: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ requirements: []
147
+ rubygems_version: 3.0.8
148
+ signing_key:
149
+ specification_version: 4
150
+ summary: A framework for producing and publishing feeds on Solidus.
151
+ test_files:
152
+ - spec/lib/solidus_feeds/generators/google_merchant_spec.rb
153
+ - spec/lib/solidus_feeds/publishers/s3_spec.rb
154
+ - spec/lib/solidus_feeds/publishers/static_file_spec.rb
155
+ - spec/lib/solidus_feeds/solidus_feeds_spec.rb
156
+ - spec/solidus_feeds_spec.rb
157
+ - spec/spec_helper.rb