solidus_feeds 0.1.0

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.
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