metal_archives 2.2.0 → 3.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +93 -0
  3. data/.gitignore +6 -6
  4. data/.overcommit.yml +35 -0
  5. data/.rspec +2 -0
  6. data/.rubocop.yml +66 -6
  7. data/CHANGELOG.md +33 -0
  8. data/Gemfile +1 -1
  9. data/LICENSE.md +17 -4
  10. data/README.md +65 -86
  11. data/Rakefile +8 -7
  12. data/bin/console +38 -0
  13. data/bin/setup +8 -0
  14. data/config/inflections.rb +7 -0
  15. data/config/initializers/.keep +0 -0
  16. data/docker-compose.yml +23 -0
  17. data/lib/metal_archives.rb +82 -27
  18. data/lib/metal_archives/cache/base.rb +40 -0
  19. data/lib/metal_archives/cache/memory.rb +68 -0
  20. data/lib/metal_archives/cache/null.rb +22 -0
  21. data/lib/metal_archives/cache/redis.rb +49 -0
  22. data/lib/metal_archives/{utils/collection.rb → collection.rb} +3 -5
  23. data/lib/metal_archives/configuration.rb +33 -50
  24. data/lib/metal_archives/{error.rb → errors.rb} +9 -1
  25. data/lib/metal_archives/http_client.rb +45 -44
  26. data/lib/metal_archives/models/artist.rb +90 -45
  27. data/lib/metal_archives/models/band.rb +77 -52
  28. data/lib/metal_archives/models/base.rb +225 -0
  29. data/lib/metal_archives/models/label.rb +14 -15
  30. data/lib/metal_archives/models/release.rb +25 -29
  31. data/lib/metal_archives/parsers/artist.rb +86 -50
  32. data/lib/metal_archives/parsers/band.rb +155 -88
  33. data/lib/metal_archives/parsers/base.rb +14 -0
  34. data/lib/metal_archives/parsers/country.rb +21 -0
  35. data/lib/metal_archives/parsers/date.rb +31 -0
  36. data/lib/metal_archives/parsers/genre.rb +67 -0
  37. data/lib/metal_archives/parsers/label.rb +39 -31
  38. data/lib/metal_archives/parsers/parser.rb +18 -63
  39. data/lib/metal_archives/parsers/release.rb +98 -89
  40. data/lib/metal_archives/parsers/year.rb +31 -0
  41. data/lib/metal_archives/version.rb +12 -1
  42. data/metal_archives.env.example +10 -0
  43. data/metal_archives.gemspec +43 -28
  44. data/nginx/default.conf +60 -0
  45. metadata +179 -74
  46. data/.travis.yml +0 -12
  47. data/lib/metal_archives/middleware/cache_check.rb +0 -20
  48. data/lib/metal_archives/middleware/encoding.rb +0 -16
  49. data/lib/metal_archives/middleware/headers.rb +0 -38
  50. data/lib/metal_archives/middleware/rewrite_endpoint.rb +0 -38
  51. data/lib/metal_archives/models/base_model.rb +0 -215
  52. data/lib/metal_archives/utils/lru_cache.rb +0 -61
  53. data/lib/metal_archives/utils/nil_date.rb +0 -99
  54. data/lib/metal_archives/utils/range.rb +0 -66
  55. data/spec/configuration_spec.rb +0 -96
  56. data/spec/factories/artist_factory.rb +0 -37
  57. data/spec/factories/band_factory.rb +0 -60
  58. data/spec/factories/nil_date_factory.rb +0 -9
  59. data/spec/factories/range_factory.rb +0 -8
  60. data/spec/models/artist_spec.rb +0 -138
  61. data/spec/models/band_spec.rb +0 -164
  62. data/spec/models/base_model_spec.rb +0 -219
  63. data/spec/models/release_spec.rb +0 -133
  64. data/spec/parser_spec.rb +0 -19
  65. data/spec/spec_helper.rb +0 -111
  66. data/spec/support/factory_girl.rb +0 -5
  67. data/spec/support/metal_archives.rb +0 -33
  68. data/spec/utils/collection_spec.rb +0 -72
  69. data/spec/utils/lru_cache_spec.rb +0 -53
  70. data/spec/utils/nil_date_spec.rb +0 -156
  71. data/spec/utils/range_spec.rb +0 -62
data/Rakefile CHANGED
@@ -1,16 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rake'
4
- require 'rake/testtask'
3
+ require "rake"
4
+ require "rake/testtask"
5
+ require "zeitwerk"
5
6
 
6
7
  Rake::TestTask.new do |t|
7
- t.libs << 'test'
8
- t.test_files = FileList['test/*/*_test.rb']
8
+ t.libs << "test"
9
+ t.test_files = FileList["test/*/*_test.rb"]
9
10
  t.verbose = true
10
11
  end
11
12
 
12
- require 'rdoc/task'
13
+ require "rdoc/task"
13
14
  RDoc::Task.new do |rdoc|
14
- rdoc.main = 'README.md'
15
- rdoc.rdoc_files.include('README.md', 'lib/**/*.rb')
15
+ rdoc.main = "README.md"
16
+ rdoc.rdoc_files.include("README.md", "lib/**/*.rb")
16
17
  end
data/bin/console ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "logger"
5
+
6
+ require "bundler/setup"
7
+ require "metal_archives"
8
+
9
+ # You can add fixtures and/or initialization code here to make experimenting
10
+ # with your gem easier. You can also use a different console, if you like.
11
+
12
+ MetalArchives.configure do |c|
13
+ ## Application identity (required)
14
+ c.app_name = "My App"
15
+ c.app_version = "1.0"
16
+ c.app_contact = "support@mymusicapp.com"
17
+
18
+ ## Enable Redis as caching backend (optional, overrides default memory cache)
19
+ ## Available cache strategies: :memory, :redis or :null (disable caching)
20
+ # c.cache_strategy = :redis
21
+ # c.cache_options = { url: "redis://redis:6379", ttl: 1.month.to_i }
22
+
23
+ ## Metal Archives endpoint (optional, overrides default)
24
+ c.endpoint = ENV["MA_ENDPOINT"] if ENV["MA_ENDPOINT"]
25
+ c.endpoint_user = ENV["MA_ENDPOINT_USER"] if ENV["MA_ENDPOINT_USER"]
26
+ c.endpoint_password = ENV["MA_ENDPOINT_PASSWORD"] if ENV["MA_ENDPOINT_PASSWORD"]
27
+
28
+ ## Custom logger (optional)
29
+ c.logger = Logger.new $stdout
30
+ c.logger.level = Logger::WARN
31
+ end
32
+
33
+ # (If you use this, don't forget to add pry to your Gemfile!)
34
+ # require "pry"
35
+ # Pry.start
36
+
37
+ require "irb"
38
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ MetalArchives.loader.inflector.inflect(
4
+ "id" => "ID",
5
+ "http_client" => "HTTPClient",
6
+ "lru_cache" => "LRUCache",
7
+ )
File without changes
@@ -0,0 +1,23 @@
1
+ version: "3.7"
2
+
3
+ services:
4
+ nginx:
5
+ image: nginx:alpine
6
+ ports:
7
+ - "0.0.0.0:8080:80"
8
+ volumes:
9
+ - ./nginx/:/etc/nginx/conf.d/
10
+ - nginx:/cache/
11
+ restart: always
12
+
13
+ redis:
14
+ image: redis:alpine
15
+ ports:
16
+ - "0.0.0.0:6379:6379"
17
+ volumes:
18
+ - redis:/data/
19
+ restart: always
20
+
21
+ volumes:
22
+ nginx:
23
+ redis:
@@ -1,37 +1,92 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'openssl'
3
+ require "zeitwerk"
4
+ require "byebug" if ENV["METAL_ARCHIVES_ENV"] == "development"
5
+ require "active_support/all"
4
6
 
5
- require 'metal_archives/middleware/headers'
6
- require 'metal_archives/middleware/cache_check'
7
- require 'metal_archives/middleware/rewrite_endpoint'
8
- require 'metal_archives/middleware/encoding'
7
+ ##
8
+ # Metal Archives Ruby API
9
+ #
10
+ module MetalArchives
11
+ class << self
12
+ # Code loader instance
13
+ attr_reader :loader
9
14
 
10
- require 'metal_archives/version'
11
- require 'metal_archives/configuration'
12
- require 'metal_archives/error'
15
+ ##
16
+ # Root path
17
+ #
18
+ def root
19
+ @root ||= Pathname.new(File.expand_path(File.join("..", ".."), __FILE__))
20
+ end
13
21
 
14
- require 'metal_archives/utils/range'
15
- require 'metal_archives/utils/collection'
16
- require 'metal_archives/utils/lru_cache'
17
- require 'metal_archives/utils/nil_date'
22
+ ##
23
+ # HTTP client
24
+ #
25
+ def http
26
+ @http ||= HTTPClient.new
27
+ end
18
28
 
19
- require 'metal_archives/models/base_model'
20
- require 'metal_archives/models/label'
21
- require 'metal_archives/models/artist'
22
- require 'metal_archives/models/band'
23
- require 'metal_archives/models/release'
29
+ ##
30
+ # API configuration
31
+ #
32
+ # Instance of rdoc-ref:MetalArchives::Configuration
33
+ #
34
+ def config
35
+ @config ||= Configuration.new
36
+ end
24
37
 
25
- require 'metal_archives/parsers/parser'
26
- require 'metal_archives/parsers/label'
27
- require 'metal_archives/parsers/artist'
28
- require 'metal_archives/parsers/band'
29
- require 'metal_archives/parsers/release'
38
+ ##
39
+ # Cache instance
40
+ #
41
+ def cache
42
+ raise MetalArchives::Errors::InvalidConfigurationError, "cache has not been configured" unless config.cache_strategy
30
43
 
31
- require 'metal_archives/http_client'
44
+ @cache ||= Cache
45
+ .const_get(loader.inflector.camelize(config.cache_strategy, root))
46
+ .new(config.cache_options)
47
+ end
32
48
 
33
- ##
34
- # Metal Archives Ruby API
35
- #
36
- module MetalArchives
49
+ ##
50
+ # Configure API options.
51
+ #
52
+ # A block must be specified, to which a
53
+ # rdoc-ref:MetalArchives::Configuration parameter will be passed.
54
+ #
55
+ # [Raises]
56
+ # - rdoc-ref:InvalidConfigurationException
57
+ #
58
+ def configure
59
+ raise Errors::InvalidConfigurationError, "no configuration block given" unless block_given?
60
+
61
+ yield config
62
+
63
+ config.validate!
64
+ end
65
+
66
+ ##
67
+ # Set up application framework
68
+ #
69
+ def setup
70
+ @loader = Zeitwerk::Loader.for_gem
71
+
72
+ # Register inflections
73
+ require root.join("config/inflections.rb")
74
+
75
+ # Set up code loader
76
+ loader.enable_reloading if ENV["METAL_ARCHIVES_ENV"] == "development"
77
+ loader.collapse(root.join("lib/metal_archives/models"))
78
+ loader.do_not_eager_load(root.join("lib/metal_archives/cache"))
79
+ loader.setup
80
+ loader.eager_load
81
+
82
+ # Load initializers
83
+ Dir[root.join("config/initializers/*.rb")].sort.each { |f| require f }
84
+ end
85
+ end
86
+ end
87
+
88
+ def reload!
89
+ MetalArchives.loader.reload
37
90
  end
91
+
92
+ MetalArchives.setup
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MetalArchives
4
+ module Cache
5
+ ##
6
+ # Generic cache interface
7
+ #
8
+ class Base
9
+ attr_accessor :options
10
+
11
+ def initialize(options = {})
12
+ @options = options
13
+
14
+ validate!
15
+ end
16
+
17
+ def validate!; end
18
+
19
+ def []
20
+ raise NotImplementedError
21
+ end
22
+
23
+ def []=(_key, _value)
24
+ raise NotImplementedError
25
+ end
26
+
27
+ def clear
28
+ raise NotImplementedError
29
+ end
30
+
31
+ def include?(_key)
32
+ raise NotImplementedError
33
+ end
34
+
35
+ def delete(_key)
36
+ raise NotImplementedError
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MetalArchives
4
+ module Cache
5
+ ##
6
+ # Generic LRU memory cache
7
+ #
8
+ class Memory < Base
9
+ def validate!
10
+ raise Errors::InvalidConfigurationError, "size has not been configured" if options[:size].blank?
11
+ raise Errors::InvalidConfigurationError, "size must be a number" unless options[:size].is_a? Integer
12
+ end
13
+
14
+ def [](key)
15
+ if keys.include? key
16
+ MetalArchives.config.logger.debug "Cache hit for #{key}"
17
+ keys.delete key
18
+ keys << key
19
+ else
20
+ MetalArchives.config.logger.debug "Cache miss for #{key}"
21
+ end
22
+
23
+ cache[key]
24
+ end
25
+
26
+ def []=(key, value)
27
+ cache[key] = value
28
+
29
+ keys.delete key if keys.include? key
30
+
31
+ keys << key
32
+
33
+ pop if keys.size > options[:size]
34
+ end
35
+
36
+ def clear
37
+ cache.clear
38
+ keys.clear
39
+ end
40
+
41
+ def include?(key)
42
+ cache.include? key
43
+ end
44
+
45
+ def delete(key)
46
+ cache.delete key
47
+ end
48
+
49
+ private
50
+
51
+ def cache
52
+ # Underlying data store
53
+ @cache ||= {}
54
+ end
55
+
56
+ def keys
57
+ # Array of keys in order of insertion
58
+ @keys ||= []
59
+ end
60
+
61
+ def pop
62
+ to_remove = keys.shift(keys.size - options[:size])
63
+
64
+ to_remove.each { |key| cache.delete key }
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MetalArchives
4
+ module Cache
5
+ ##
6
+ # Null cache
7
+ #
8
+ class Null < Base
9
+ def [](_key); end
10
+
11
+ def []=(_key, _value); end
12
+
13
+ def clear; end
14
+
15
+ def include?(_key)
16
+ false
17
+ end
18
+
19
+ def delete(_key); end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "redis"
4
+
5
+ module MetalArchives
6
+ module Cache
7
+ ##
8
+ # Redis-backed cache
9
+ #
10
+ class Redis < Base
11
+ def initialize(options = {})
12
+ super
13
+
14
+ # Default TTL is 1 month
15
+ options[:ttl] ||= (30 * 24 * 60 * 60)
16
+ end
17
+
18
+ def [](key)
19
+ redis.get cache_key_for(key)
20
+ end
21
+
22
+ def []=(key, value)
23
+ redis.set cache_key_for(key), value, ex: options[:ttl]
24
+ end
25
+
26
+ def clear
27
+ redis.keys(cache_key_for("*")).each { |key| redis.del key }
28
+ end
29
+
30
+ def include?(key)
31
+ redis.exists? cache_key_for(key)
32
+ end
33
+
34
+ def delete(key)
35
+ redis.del cache_key_for(key)
36
+ end
37
+
38
+ private
39
+
40
+ def cache_key_for(key)
41
+ "metal_archives.cache.#{key}"
42
+ end
43
+
44
+ def redis
45
+ @redis ||= ::Redis.new(**options.except(:ttl))
46
+ end
47
+ end
48
+ end
49
+ end
@@ -22,15 +22,13 @@ module MetalArchives
22
22
  # Calls the given block once for each element, passing that element as a parameter.
23
23
  # If no block is given, an Enumerator is returned.
24
24
  #
25
- def each
26
- return to_enum :each unless block_given?
25
+ def each(&block)
26
+ return to_enum :each unless block
27
27
 
28
28
  loop do
29
29
  items = instance_exec(&@proc)
30
30
 
31
- items.each do |item|
32
- yield item
33
- end
31
+ items.each(&block)
34
32
 
35
33
  break if items.empty?
36
34
  end
@@ -1,37 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module MetalArchives
4
- class << self
5
- ##
6
- # API configuration
7
- #
8
- # Instance of rdoc-ref:MetalArchives::Configuration
9
- #
10
- def config
11
- raise MetalArchives::Errors::InvalidConfigurationError, 'Gem has not been configured' unless @config
3
+ require "logger"
12
4
 
13
- @config
14
- end
15
-
16
- ##
17
- # Configure API options.
18
- #
19
- # A block must be specified, to which a
20
- # rdoc-ref:MetalArchives::Configuration parameter will be passed.
21
- #
22
- # [Raises]
23
- # - rdoc-ref:InvalidConfigurationException
24
- #
25
- def configure
26
- raise MetalArchives::Errors::InvalidConfigurationError, 'No configuration block given' unless block_given?
27
- @config = MetalArchives::Configuration.new
28
- yield @config
29
-
30
- raise MetalArchives::Errors::InvalidConfigurationError, 'app_name has not been configured' unless MetalArchives.config.app_name && !MetalArchives.config.app_name.empty?
31
- raise MetalArchives::Errors::InvalidConfigurationError, 'app_version has not been configured' unless MetalArchives.config.app_version && !MetalArchives.config.app_version.empty?
32
- raise MetalArchives::Errors::InvalidConfigurationError, 'app_contact has not been configured' unless MetalArchives.config.app_contact && !MetalArchives.config.app_contact.empty?
33
- end
34
- end
5
+ module MetalArchives
6
+ CACHE_STRATEGIES = %w(memory redis).freeze
35
7
 
36
8
  ##
37
9
  # Contains configuration options
@@ -53,45 +25,56 @@ module MetalArchives
53
25
  attr_accessor :app_contact
54
26
 
55
27
  ##
56
- # Override Metal Archives endpoint (defaults to http://www.metal-archives.com/)
28
+ # Override Metal Archives endpoint (defaults to https://www.metal-archives.com/)
57
29
  #
58
30
  attr_accessor :endpoint
59
- attr_reader :default_endpoint
60
31
 
61
32
  ##
62
- # Additional Faraday middleware
33
+ # Endpoint HTTP Basic authentication
63
34
  #
64
- attr_accessor :middleware
35
+ attr_accessor :endpoint_user
36
+ attr_accessor :endpoint_password
65
37
 
66
38
  ##
67
- # Request throttling rate (in seconds per request per path)
39
+ # Logger instance
68
40
  #
69
- attr_accessor :request_rate
41
+ attr_accessor :logger
70
42
 
71
43
  ##
72
- # Request timeout (in seconds per request per path)
44
+ # Cache strategy
73
45
  #
74
- attr_accessor :request_timeout
46
+ attr_accessor :cache_strategy
75
47
 
76
48
  ##
77
- # Logger instance
49
+ # Cache strategy options
78
50
  #
79
- attr_accessor :logger
51
+ attr_accessor :cache_options
80
52
 
81
53
  ##
82
- # Cache size (per object class)
54
+ # Default configuration values
83
55
  #
84
- attr_accessor :cache_size
56
+ def initialize(**attributes)
57
+ attributes.each { |key, value| send(:"#{key}=", value) }
58
+
59
+ @endpoint ||= "https://www.metal-archives.com/"
60
+ @logger ||= Logger.new $stdout
61
+
62
+ @cache_strategy ||= "memory"
63
+ @cache_options ||= { size: 100 }
64
+ end
85
65
 
86
66
  ##
87
- # Default configuration values
67
+ # Validate configuration
68
+ #
69
+ # [Raises]
70
+ # - rdoc-ref:MetalArchives::Errors::ConfigurationError when configuration is invalid
88
71
  #
89
- def initialize
90
- @default_endpoint = 'https://www.metal-archives.com/'
91
- @throttle_rate = 1
92
- @throttle_wait = 3
93
- @logger = Logger.new STDOUT
94
- @cache_size = 100
72
+ def validate!
73
+ raise Errors::InvalidConfigurationError, "app_name has not been configured" if app_name.blank?
74
+ raise Errors::InvalidConfigurationError, "app_version has not been configured" if app_version.blank?
75
+ raise Errors::InvalidConfigurationError, "app_contact has not been configured" if app_contact.blank?
76
+ raise Errors::InvalidConfigurationError, "cache_strategy has not been configured" if cache_strategy.blank?
77
+ raise Errors::InvalidConfigurationError, "cache_strategy must be one of: #{CACHE_STRATEGIES.join(', ')}" if CACHE_STRATEGIES.exclude?(cache_strategy.to_s)
95
78
  end
96
79
  end
97
80
  end