metal_archives 2.1.1 → 3.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 (70) 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 +69 -6
  7. data/CHANGELOG.md +29 -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 -25
  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 +80 -55
  28. data/lib/metal_archives/models/base.rb +218 -0
  29. data/lib/metal_archives/models/label.rb +14 -15
  30. data/lib/metal_archives/models/release.rb +349 -0
  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 +16 -63
  39. data/lib/metal_archives/parsers/release.rb +242 -0
  40. data/lib/metal_archives/parsers/year.rb +29 -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 +181 -72
  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/parser_spec.rb +0 -19
  64. data/spec/spec_helper.rb +0 -111
  65. data/spec/support/factory_girl.rb +0 -5
  66. data/spec/support/metal_archives.rb +0 -33
  67. data/spec/utils/collection_spec.rb +0 -72
  68. data/spec/utils/lru_cache_spec.rb +0 -53
  69. data/spec/utils/nil_date_spec.rb +0 -156
  70. 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,35 +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'
29
+ ##
30
+ # API configuration
31
+ #
32
+ # Instance of rdoc-ref:MetalArchives::Configuration
33
+ #
34
+ def config
35
+ @config ||= Configuration.new
36
+ end
23
37
 
24
- require 'metal_archives/parsers/parser'
25
- require 'metal_archives/parsers/label'
26
- require 'metal_archives/parsers/artist'
27
- require 'metal_archives/parsers/band'
38
+ ##
39
+ # Cache instance
40
+ #
41
+ def cache
42
+ raise MetalArchives::Errors::InvalidConfigurationError, "cache has not been configured" unless config.cache_strategy
28
43
 
29
- 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
30
48
 
31
- ##
32
- # Metal Archives Ruby API
33
- #
34
- 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
35
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