metal_archives 2.1.1 → 3.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/workflows/ci.yml +93 -0
- data/.gitignore +6 -6
- data/.overcommit.yml +35 -0
- data/.rspec +2 -0
- data/.rubocop.yml +69 -6
- data/CHANGELOG.md +29 -0
- data/Gemfile +1 -1
- data/LICENSE.md +17 -4
- data/README.md +65 -86
- data/Rakefile +8 -7
- data/bin/console +38 -0
- data/bin/setup +8 -0
- data/config/inflections.rb +7 -0
- data/config/initializers/.keep +0 -0
- data/docker-compose.yml +23 -0
- data/lib/metal_archives.rb +82 -25
- data/lib/metal_archives/cache/base.rb +40 -0
- data/lib/metal_archives/cache/memory.rb +68 -0
- data/lib/metal_archives/cache/null.rb +22 -0
- data/lib/metal_archives/cache/redis.rb +49 -0
- data/lib/metal_archives/{utils/collection.rb → collection.rb} +3 -5
- data/lib/metal_archives/configuration.rb +33 -50
- data/lib/metal_archives/{error.rb → errors.rb} +9 -1
- data/lib/metal_archives/http_client.rb +45 -44
- data/lib/metal_archives/models/artist.rb +90 -45
- data/lib/metal_archives/models/band.rb +80 -55
- data/lib/metal_archives/models/base.rb +218 -0
- data/lib/metal_archives/models/label.rb +14 -15
- data/lib/metal_archives/models/release.rb +349 -0
- data/lib/metal_archives/parsers/artist.rb +86 -50
- data/lib/metal_archives/parsers/band.rb +155 -88
- data/lib/metal_archives/parsers/base.rb +14 -0
- data/lib/metal_archives/parsers/country.rb +21 -0
- data/lib/metal_archives/parsers/date.rb +31 -0
- data/lib/metal_archives/parsers/genre.rb +67 -0
- data/lib/metal_archives/parsers/label.rb +39 -31
- data/lib/metal_archives/parsers/parser.rb +16 -63
- data/lib/metal_archives/parsers/release.rb +242 -0
- data/lib/metal_archives/parsers/year.rb +29 -0
- data/lib/metal_archives/version.rb +12 -1
- data/metal_archives.env.example +10 -0
- data/metal_archives.gemspec +43 -28
- data/nginx/default.conf +60 -0
- metadata +181 -72
- data/.travis.yml +0 -12
- data/lib/metal_archives/middleware/cache_check.rb +0 -20
- data/lib/metal_archives/middleware/encoding.rb +0 -16
- data/lib/metal_archives/middleware/headers.rb +0 -38
- data/lib/metal_archives/middleware/rewrite_endpoint.rb +0 -38
- data/lib/metal_archives/models/base_model.rb +0 -215
- data/lib/metal_archives/utils/lru_cache.rb +0 -61
- data/lib/metal_archives/utils/nil_date.rb +0 -99
- data/lib/metal_archives/utils/range.rb +0 -66
- data/spec/configuration_spec.rb +0 -96
- data/spec/factories/artist_factory.rb +0 -37
- data/spec/factories/band_factory.rb +0 -60
- data/spec/factories/nil_date_factory.rb +0 -9
- data/spec/factories/range_factory.rb +0 -8
- data/spec/models/artist_spec.rb +0 -138
- data/spec/models/band_spec.rb +0 -164
- data/spec/models/base_model_spec.rb +0 -219
- data/spec/parser_spec.rb +0 -19
- data/spec/spec_helper.rb +0 -111
- data/spec/support/factory_girl.rb +0 -5
- data/spec/support/metal_archives.rb +0 -33
- data/spec/utils/collection_spec.rb +0 -72
- data/spec/utils/lru_cache_spec.rb +0 -53
- data/spec/utils/nil_date_spec.rb +0 -156
- 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
|
4
|
-
require
|
3
|
+
require "rake"
|
4
|
+
require "rake/testtask"
|
5
|
+
require "zeitwerk"
|
5
6
|
|
6
7
|
Rake::TestTask.new do |t|
|
7
|
-
t.libs <<
|
8
|
-
t.test_files = FileList[
|
8
|
+
t.libs << "test"
|
9
|
+
t.test_files = FileList["test/*/*_test.rb"]
|
9
10
|
t.verbose = true
|
10
11
|
end
|
11
12
|
|
12
|
-
require
|
13
|
+
require "rdoc/task"
|
13
14
|
RDoc::Task.new do |rdoc|
|
14
|
-
rdoc.main =
|
15
|
-
rdoc.rdoc_files.include(
|
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
File without changes
|
data/docker-compose.yml
ADDED
@@ -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:
|
data/lib/metal_archives.rb
CHANGED
@@ -1,35 +1,92 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "zeitwerk"
|
4
|
+
require "byebug" if ENV["METAL_ARCHIVES_ENV"] == "development"
|
5
|
+
require "active_support/all"
|
4
6
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
7
|
+
##
|
8
|
+
# Metal Archives Ruby API
|
9
|
+
#
|
10
|
+
module MetalArchives
|
11
|
+
class << self
|
12
|
+
# Code loader instance
|
13
|
+
attr_reader :loader
|
9
14
|
|
10
|
-
|
11
|
-
|
12
|
-
|
15
|
+
##
|
16
|
+
# Root path
|
17
|
+
#
|
18
|
+
def root
|
19
|
+
@root ||= Pathname.new(File.expand_path(File.join("..", ".."), __FILE__))
|
20
|
+
end
|
13
21
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
22
|
+
##
|
23
|
+
# HTTP client
|
24
|
+
#
|
25
|
+
def http
|
26
|
+
@http ||= HTTPClient.new
|
27
|
+
end
|
18
28
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
44
|
+
@cache ||= Cache
|
45
|
+
.const_get(loader.inflector.camelize(config.cache_strategy, root))
|
46
|
+
.new(config.cache_options)
|
47
|
+
end
|
30
48
|
|
31
|
-
##
|
32
|
-
#
|
33
|
-
#
|
34
|
-
|
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
|
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
|
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
|
-
|
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
|
-
|
14
|
-
|
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
|
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
|
-
#
|
33
|
+
# Endpoint HTTP Basic authentication
|
63
34
|
#
|
64
|
-
attr_accessor :
|
35
|
+
attr_accessor :endpoint_user
|
36
|
+
attr_accessor :endpoint_password
|
65
37
|
|
66
38
|
##
|
67
|
-
#
|
39
|
+
# Logger instance
|
68
40
|
#
|
69
|
-
attr_accessor :
|
41
|
+
attr_accessor :logger
|
70
42
|
|
71
43
|
##
|
72
|
-
#
|
44
|
+
# Cache strategy
|
73
45
|
#
|
74
|
-
attr_accessor :
|
46
|
+
attr_accessor :cache_strategy
|
75
47
|
|
76
48
|
##
|
77
|
-
#
|
49
|
+
# Cache strategy options
|
78
50
|
#
|
79
|
-
attr_accessor :
|
51
|
+
attr_accessor :cache_options
|
80
52
|
|
81
53
|
##
|
82
|
-
#
|
54
|
+
# Default configuration values
|
83
55
|
#
|
84
|
-
|
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
|
-
#
|
67
|
+
# Validate configuration
|
68
|
+
#
|
69
|
+
# [Raises]
|
70
|
+
# - rdoc-ref:MetalArchives::Errors::ConfigurationError when configuration is invalid
|
88
71
|
#
|
89
|
-
def
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
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
|