librarianp 0.1.2
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.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/.rspec +1 -0
- data/.travis.yml +10 -0
- data/CHANGELOG.md +255 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +235 -0
- data/LICENSE.txt +22 -0
- data/README.md +55 -0
- data/Rakefile +28 -0
- data/VERSION +1 -0
- data/lib/librarian/action/base.rb +24 -0
- data/lib/librarian/action/clean.rb +44 -0
- data/lib/librarian/action/ensure.rb +24 -0
- data/lib/librarian/action/install.rb +95 -0
- data/lib/librarian/action/persist_resolution_mixin.rb +51 -0
- data/lib/librarian/action/resolve.rb +46 -0
- data/lib/librarian/action/update.rb +44 -0
- data/lib/librarian/action.rb +5 -0
- data/lib/librarian/algorithms.rb +133 -0
- data/lib/librarian/cli/manifest_presenter.rb +89 -0
- data/lib/librarian/cli.rb +225 -0
- data/lib/librarian/config/database.rb +205 -0
- data/lib/librarian/config/file_source.rb +47 -0
- data/lib/librarian/config/hash_source.rb +33 -0
- data/lib/librarian/config/source.rb +149 -0
- data/lib/librarian/config.rb +7 -0
- data/lib/librarian/dependency.rb +153 -0
- data/lib/librarian/dsl/receiver.rb +42 -0
- data/lib/librarian/dsl/target.rb +171 -0
- data/lib/librarian/dsl.rb +102 -0
- data/lib/librarian/environment/runtime_cache.rb +101 -0
- data/lib/librarian/environment.rb +230 -0
- data/lib/librarian/error.rb +4 -0
- data/lib/librarian/helpers.rb +29 -0
- data/lib/librarian/linter/source_linter.rb +55 -0
- data/lib/librarian/lockfile/compiler.rb +66 -0
- data/lib/librarian/lockfile/parser.rb +123 -0
- data/lib/librarian/lockfile.rb +29 -0
- data/lib/librarian/logger.rb +46 -0
- data/lib/librarian/manifest.rb +146 -0
- data/lib/librarian/manifest_set.rb +150 -0
- data/lib/librarian/mock/cli.rb +19 -0
- data/lib/librarian/mock/dsl.rb +15 -0
- data/lib/librarian/mock/environment.rb +21 -0
- data/lib/librarian/mock/extension.rb +9 -0
- data/lib/librarian/mock/source/mock/registry.rb +83 -0
- data/lib/librarian/mock/source/mock.rb +80 -0
- data/lib/librarian/mock/source.rb +1 -0
- data/lib/librarian/mock/version.rb +5 -0
- data/lib/librarian/mock.rb +1 -0
- data/lib/librarian/posix.rb +129 -0
- data/lib/librarian/resolution.rb +46 -0
- data/lib/librarian/resolver/implementation.rb +238 -0
- data/lib/librarian/resolver.rb +94 -0
- data/lib/librarian/rspec/support/cli_macro.rb +120 -0
- data/lib/librarian/source/basic_api.rb +45 -0
- data/lib/librarian/source/git/repository.rb +193 -0
- data/lib/librarian/source/git.rb +172 -0
- data/lib/librarian/source/local.rb +54 -0
- data/lib/librarian/source/path.rb +56 -0
- data/lib/librarian/source.rb +2 -0
- data/lib/librarian/spec.rb +13 -0
- data/lib/librarian/spec_change_set.rb +173 -0
- data/lib/librarian/specfile.rb +19 -0
- data/lib/librarian/support/abstract_method.rb +21 -0
- data/lib/librarian/ui.rb +64 -0
- data/lib/librarian/version.rb +3 -0
- data/lib/librarian.rb +11 -0
- data/librarian.gemspec +47 -0
- data/spec/functional/cli_spec.rb +27 -0
- data/spec/functional/posix_spec.rb +32 -0
- data/spec/functional/source/git/repository_spec.rb +199 -0
- data/spec/functional/source/git_spec.rb +174 -0
- data/spec/support/fakefs.rb +37 -0
- data/spec/support/method_patch_macro.rb +30 -0
- data/spec/support/project_path_macro.rb +14 -0
- data/spec/support/with_env_macro.rb +22 -0
- data/spec/unit/action/base_spec.rb +18 -0
- data/spec/unit/action/clean_spec.rb +102 -0
- data/spec/unit/action/ensure_spec.rb +37 -0
- data/spec/unit/action/install_spec.rb +111 -0
- data/spec/unit/algorithms_spec.rb +131 -0
- data/spec/unit/config/database_spec.rb +320 -0
- data/spec/unit/dependency/requirement_spec.rb +12 -0
- data/spec/unit/dependency_spec.rb +212 -0
- data/spec/unit/dsl_spec.rb +173 -0
- data/spec/unit/environment/runtime_cache_spec.rb +73 -0
- data/spec/unit/environment_spec.rb +209 -0
- data/spec/unit/lockfile/parser_spec.rb +162 -0
- data/spec/unit/lockfile_spec.rb +65 -0
- data/spec/unit/manifest/version_spec.rb +11 -0
- data/spec/unit/manifest_set_spec.rb +202 -0
- data/spec/unit/manifest_spec.rb +36 -0
- data/spec/unit/mock/environment_spec.rb +25 -0
- data/spec/unit/mock/source/mock_spec.rb +22 -0
- data/spec/unit/resolver_spec.rb +299 -0
- data/spec/unit/source/git_spec.rb +29 -0
- data/spec/unit/spec_change_set_spec.rb +169 -0
- metadata +257 -0
@@ -0,0 +1,225 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'thor/actions'
|
3
|
+
|
4
|
+
require 'librarian'
|
5
|
+
require 'librarian/error'
|
6
|
+
require 'librarian/action'
|
7
|
+
require "librarian/ui"
|
8
|
+
|
9
|
+
module Librarian
|
10
|
+
class Cli < Thor
|
11
|
+
|
12
|
+
autoload :ManifestPresenter, "librarian/cli/manifest_presenter"
|
13
|
+
|
14
|
+
include Thor::Actions
|
15
|
+
|
16
|
+
module Particularity
|
17
|
+
def root_module
|
18
|
+
nil
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
extend Particularity
|
23
|
+
|
24
|
+
class << self
|
25
|
+
def bin!
|
26
|
+
status = with_environment { returning_status { start } }
|
27
|
+
exit status
|
28
|
+
end
|
29
|
+
|
30
|
+
def returning_status
|
31
|
+
yield
|
32
|
+
0
|
33
|
+
rescue Librarian::Error => e
|
34
|
+
environment.ui.error e.message
|
35
|
+
environment.ui.debug e.backtrace.join("\n")
|
36
|
+
e.respond_to?(:status_code) && e.status_code || 1
|
37
|
+
rescue Interrupt => e
|
38
|
+
environment.ui.error "\nQuitting..."
|
39
|
+
1
|
40
|
+
end
|
41
|
+
|
42
|
+
attr_accessor :environment
|
43
|
+
|
44
|
+
def with_environment
|
45
|
+
environment = root_module.environment_class.new
|
46
|
+
self.environment, orig_environment = environment, self.environment
|
47
|
+
yield(environment)
|
48
|
+
ensure
|
49
|
+
self.environment = orig_environment
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def initialize(*)
|
54
|
+
super
|
55
|
+
the_shell = (options["no-color"] ? Thor::Shell::Basic.new : shell)
|
56
|
+
environment.ui = UI::Shell.new(the_shell)
|
57
|
+
environment.ui.be_quiet! if options["quiet"]
|
58
|
+
environment.ui.debug! if options["verbose"]
|
59
|
+
environment.ui.debug_line_numbers! if options["verbose"] && options["line-numbers"]
|
60
|
+
|
61
|
+
write_debug_header
|
62
|
+
end
|
63
|
+
|
64
|
+
desc "version", "Displays the version."
|
65
|
+
def version
|
66
|
+
say "librarian-#{environment.version}"
|
67
|
+
say "librarian-#{environment.adapter_name}-#{environment.adapter_version}"
|
68
|
+
end
|
69
|
+
|
70
|
+
desc "config", "Show or edit the config."
|
71
|
+
option "verbose", :type => :boolean, :default => false
|
72
|
+
option "line-numbers", :type => :boolean, :default => false
|
73
|
+
option "global", :type => :boolean, :default => false
|
74
|
+
option "local", :type => :boolean, :default => false
|
75
|
+
option "delete", :type => :boolean, :default => false
|
76
|
+
def config(key = nil, value = nil)
|
77
|
+
if key
|
78
|
+
raise Error, "cannot set both value and delete" if value && options["delete"]
|
79
|
+
if options["delete"]
|
80
|
+
scope = config_scope(true)
|
81
|
+
environment.config_db[key, scope] = nil
|
82
|
+
elsif value
|
83
|
+
scope = config_scope(true)
|
84
|
+
environment.config_db[key, scope] = value
|
85
|
+
else
|
86
|
+
scope = config_scope(false)
|
87
|
+
if value = environment.config_db[key, scope]
|
88
|
+
prefix = scope ? "#{key} (#{scope})" : key
|
89
|
+
say "#{prefix}: #{value}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
else
|
93
|
+
environment.config_db.keys.each do |key|
|
94
|
+
say "#{key}: #{environment.config_db[key]}"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
desc "clean", "Cleans out the cache and install paths."
|
100
|
+
option "verbose", :type => :boolean, :default => false
|
101
|
+
option "line-numbers", :type => :boolean, :default => false
|
102
|
+
def clean
|
103
|
+
ensure!
|
104
|
+
clean!
|
105
|
+
end
|
106
|
+
|
107
|
+
desc "update", "Updates and installs the dependencies you specify."
|
108
|
+
option "verbose", :type => :boolean, :default => false
|
109
|
+
option "line-numbers", :type => :boolean, :default => false
|
110
|
+
def update(*names)
|
111
|
+
ensure!
|
112
|
+
if names.empty?
|
113
|
+
resolve!(:force => true)
|
114
|
+
else
|
115
|
+
update!(:names => names)
|
116
|
+
end
|
117
|
+
install!
|
118
|
+
end
|
119
|
+
|
120
|
+
desc "outdated", "Lists outdated dependencies."
|
121
|
+
option "verbose", :type => :boolean, :default => false
|
122
|
+
option "line-numbers", :type => :boolean, :default => false
|
123
|
+
def outdated
|
124
|
+
ensure!
|
125
|
+
resolution = environment.lock
|
126
|
+
manifests = resolution.manifests.sort_by(&:name)
|
127
|
+
manifests.select(&:outdated?).each do |manifest|
|
128
|
+
say "#{manifest.name} (#{manifest.version} -> #{manifest.latest.version})"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
desc "show", "Shows dependencies"
|
133
|
+
option "verbose", :type => :boolean, :default => false
|
134
|
+
option "line-numbers", :type => :boolean, :default => false
|
135
|
+
option "detailed", :type => :boolean
|
136
|
+
def show(*names)
|
137
|
+
ensure!
|
138
|
+
if environment.lockfile_path.file?
|
139
|
+
manifest_presenter.present(names, :detailed => options["detailed"])
|
140
|
+
else
|
141
|
+
raise Error, "Be sure to install first!"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
desc "init", "Initializes the current directory."
|
146
|
+
def init
|
147
|
+
puts "Nothing to do."
|
148
|
+
end
|
149
|
+
|
150
|
+
private
|
151
|
+
|
152
|
+
def environment
|
153
|
+
self.class.environment
|
154
|
+
end
|
155
|
+
|
156
|
+
def ensure!(options = { })
|
157
|
+
Action::Ensure.new(environment, options).run
|
158
|
+
end
|
159
|
+
|
160
|
+
def clean!(options = { })
|
161
|
+
Action::Clean.new(environment, options).run
|
162
|
+
end
|
163
|
+
|
164
|
+
def install!(options = { })
|
165
|
+
Action::Install.new(environment, options).run
|
166
|
+
end
|
167
|
+
|
168
|
+
def resolve!(options = { })
|
169
|
+
Action::Resolve.new(environment, options).run
|
170
|
+
end
|
171
|
+
|
172
|
+
def update!(options = { })
|
173
|
+
Action::Update.new(environment, options).run
|
174
|
+
end
|
175
|
+
|
176
|
+
def manifest_presenter
|
177
|
+
ManifestPresenter.new(self, environment.lock.manifests)
|
178
|
+
end
|
179
|
+
|
180
|
+
def write_debug_header
|
181
|
+
debug { "Ruby Version: #{RUBY_VERSION}" }
|
182
|
+
debug { "Ruby Platform: #{RUBY_PLATFORM}" }
|
183
|
+
debug { "Rubinius Version: #{Rubinius::VERSION}" } if defined?(Rubinius)
|
184
|
+
debug { "JRuby Version: #{JRUBY_VERSION}" } if defined?(JRUBY_VERSION)
|
185
|
+
debug { "Rubygems Version: #{Gem::VERSION}" }
|
186
|
+
debug { "Librarian Version: #{environment.version}" }
|
187
|
+
debug { "Librarian Adapter: #{environment.adapter_name}"}
|
188
|
+
debug { "Librarian Adapter Version: #{environment.adapter_version}" }
|
189
|
+
debug { "Project: #{environment.project_path}" }
|
190
|
+
debug { "Specfile: #{relative_path_to(environment.specfile_path)}" }
|
191
|
+
debug { "Lockfile: #{relative_path_to(environment.lockfile_path)}" }
|
192
|
+
debug { "Git: #{Source::Git::Repository.bin}" }
|
193
|
+
debug { "Git Version: #{Source::Git::Repository.git_version}" }
|
194
|
+
debug { "Git Environment Variables:" }
|
195
|
+
git_env = ENV.to_a.select{|(k, v)| k =~ /\AGIT/}.sort_by{|(k, v)| k}
|
196
|
+
if git_env.empty?
|
197
|
+
debug { " (empty)" }
|
198
|
+
else
|
199
|
+
git_env.each do |(k, v)|
|
200
|
+
debug { " #{k}=#{v}"}
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def debug(*args, &block)
|
206
|
+
environment.logger.debug(*args, &block)
|
207
|
+
end
|
208
|
+
|
209
|
+
def relative_path_to(path)
|
210
|
+
environment.logger.relative_path_to(path)
|
211
|
+
end
|
212
|
+
|
213
|
+
def config_scope(exclusive)
|
214
|
+
g, l = "global", "local"
|
215
|
+
if exclusive
|
216
|
+
options[g] ^ options[l] or raise Error, "must set either #{g} or #{l}"
|
217
|
+
else
|
218
|
+
options[g] && options[l] and raise Error, "cannot set both #{g} and #{l}"
|
219
|
+
end
|
220
|
+
|
221
|
+
options[g] ? :global : options[l] ? :local : nil
|
222
|
+
end
|
223
|
+
|
224
|
+
end
|
225
|
+
end
|
@@ -0,0 +1,205 @@
|
|
1
|
+
require "pathname"
|
2
|
+
|
3
|
+
require "librarian/config/file_source"
|
4
|
+
require "librarian/config/hash_source"
|
5
|
+
|
6
|
+
module Librarian
|
7
|
+
module Config
|
8
|
+
class Database
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def library
|
12
|
+
name.split("::").first.downcase
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_accessor :adapter_name
|
17
|
+
private :adapter_name=
|
18
|
+
|
19
|
+
attr_accessor :root, :assigned_specfile_name
|
20
|
+
private :root=, :assigned_specfile_name=
|
21
|
+
|
22
|
+
attr_accessor :underlying_env, :underlying_pwd, :underlying_home
|
23
|
+
private :underlying_env=, :underlying_pwd=, :underlying_home=
|
24
|
+
|
25
|
+
def initialize(adapter_name, options = { })
|
26
|
+
self.adapter_name = adapter_name or raise ArgumentError, "must provide adapter_name"
|
27
|
+
|
28
|
+
options[:project_path] || options[:pwd] or raise ArgumentError, "must provide project_path or pwd"
|
29
|
+
|
30
|
+
self.root = options[:project_path] && Pathname(options[:project_path])
|
31
|
+
self.assigned_specfile_name = options[:specfile_name]
|
32
|
+
self.underlying_env = options[:env] or raise ArgumentError, "must provide env"
|
33
|
+
self.underlying_pwd = options[:pwd] && Pathname(options[:pwd])
|
34
|
+
self.underlying_home = options[:home] && Pathname(options[:home])
|
35
|
+
end
|
36
|
+
|
37
|
+
def global
|
38
|
+
memo(__method__) { new_file_source(global_config_path) }
|
39
|
+
end
|
40
|
+
|
41
|
+
def env
|
42
|
+
memo(__method__) { HashSource.new(adapter_name, :name => "environment", :raw => env_source_data) }
|
43
|
+
end
|
44
|
+
|
45
|
+
def local
|
46
|
+
memo(__method__) { new_file_source(local_config_path) }
|
47
|
+
end
|
48
|
+
|
49
|
+
def [](key, scope = nil)
|
50
|
+
case scope
|
51
|
+
when "local", :local then local[key]
|
52
|
+
when "env", :env then env[key]
|
53
|
+
when "global", :global then global[key]
|
54
|
+
when nil then local[key] || env[key] || global[key]
|
55
|
+
else raise Error, "bad scope"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def []=(key, scope, value)
|
60
|
+
case scope
|
61
|
+
when "local", :local then local[key] = value
|
62
|
+
when "global", :global then global[key] = value
|
63
|
+
else raise Error, "bad scope"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def keys
|
68
|
+
[local, env, global].inject([]){|a, e| a.concat(e.keys) ; a}.sort.uniq
|
69
|
+
end
|
70
|
+
|
71
|
+
def project_path
|
72
|
+
root || specfile_path.dirname
|
73
|
+
end
|
74
|
+
|
75
|
+
def specfile_path
|
76
|
+
if root
|
77
|
+
root + (assigned_specfile_name || default_specfile_name)
|
78
|
+
else
|
79
|
+
env_specfile_path || default_specfile_path
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def specfile_name
|
84
|
+
specfile_path.basename.to_s
|
85
|
+
end
|
86
|
+
|
87
|
+
def lockfile_path
|
88
|
+
project_path + lockfile_name
|
89
|
+
end
|
90
|
+
|
91
|
+
def lockfile_name
|
92
|
+
"#{specfile_name}.lock"
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def new_file_source(config_path)
|
98
|
+
return unless config_path
|
99
|
+
|
100
|
+
FileSource.new(adapter_name,
|
101
|
+
:config_path => config_path,
|
102
|
+
:forbidden_keys => [config_key, specfile_key]
|
103
|
+
)
|
104
|
+
end
|
105
|
+
|
106
|
+
def global_config_path
|
107
|
+
env_global_config_path || default_global_config_path
|
108
|
+
end
|
109
|
+
|
110
|
+
def env_global_config_path
|
111
|
+
memo(__method__) { env[config_key] }
|
112
|
+
end
|
113
|
+
|
114
|
+
def default_global_config_path
|
115
|
+
underlying_home && underlying_home + config_name
|
116
|
+
end
|
117
|
+
|
118
|
+
def local_config_path
|
119
|
+
root_local_config_path || env_local_config_path || default_local_config_path
|
120
|
+
end
|
121
|
+
|
122
|
+
def root_local_config_path
|
123
|
+
root && root + config_name
|
124
|
+
end
|
125
|
+
|
126
|
+
def env_specfile_path
|
127
|
+
memo(__method__) do
|
128
|
+
path = env[specfile_key]
|
129
|
+
path && Pathname(path)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def default_specfile_path
|
134
|
+
default_project_root_path + (assigned_specfile_name || default_specfile_name)
|
135
|
+
end
|
136
|
+
|
137
|
+
def env_local_config_path
|
138
|
+
return unless env_specfile_path
|
139
|
+
|
140
|
+
env_specfile_path.dirname + config_name
|
141
|
+
end
|
142
|
+
|
143
|
+
def default_local_config_path
|
144
|
+
default_project_root_path + config_name
|
145
|
+
end
|
146
|
+
|
147
|
+
def default_project_root_path
|
148
|
+
if root
|
149
|
+
root
|
150
|
+
else
|
151
|
+
path = underlying_pwd
|
152
|
+
path = path.dirname until project_root_path?(path) || path.dirname == path
|
153
|
+
project_root_path?(path) ? path : underlying_pwd
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def project_root_path?(path)
|
158
|
+
File.file?(path + default_specfile_name)
|
159
|
+
end
|
160
|
+
|
161
|
+
def config_key
|
162
|
+
"config"
|
163
|
+
end
|
164
|
+
|
165
|
+
def specfile_key
|
166
|
+
"#{adapter_name}file"
|
167
|
+
end
|
168
|
+
|
169
|
+
def default_specfile_name
|
170
|
+
"#{adapter_name.capitalize}file"
|
171
|
+
end
|
172
|
+
|
173
|
+
def library
|
174
|
+
self.class.library
|
175
|
+
end
|
176
|
+
|
177
|
+
def config_name_prefix
|
178
|
+
".#{library}"
|
179
|
+
end
|
180
|
+
|
181
|
+
def config_name
|
182
|
+
File.join(*[config_name_prefix, adapter_name, "config"])
|
183
|
+
end
|
184
|
+
|
185
|
+
def raw_key_prefix
|
186
|
+
"#{library.upcase}_#{adapter_name.upcase}_"
|
187
|
+
end
|
188
|
+
|
189
|
+
def env_source_data
|
190
|
+
prefix = raw_key_prefix
|
191
|
+
|
192
|
+
data = underlying_env.dup
|
193
|
+
data.reject!{|k, _| !k.start_with?(prefix) || k.size <= prefix.size}
|
194
|
+
data
|
195
|
+
end
|
196
|
+
|
197
|
+
def memo(key)
|
198
|
+
key = "@#{key}"
|
199
|
+
instance_variable_set(key, yield) unless instance_variable_defined?(key)
|
200
|
+
instance_variable_get(key)
|
201
|
+
end
|
202
|
+
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require "yaml"
|
2
|
+
|
3
|
+
require "librarian/config/source"
|
4
|
+
|
5
|
+
module Librarian
|
6
|
+
module Config
|
7
|
+
class FileSource < Source
|
8
|
+
|
9
|
+
attr_accessor :config_path
|
10
|
+
private :config_path=
|
11
|
+
|
12
|
+
def initialize(adapter_name, options = { })
|
13
|
+
super
|
14
|
+
|
15
|
+
self.config_path = options.delete(:config_path) or raise ArgumentError, "must provide config_path"
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
config_path
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def load
|
25
|
+
return { } unless File.file?(config_path)
|
26
|
+
|
27
|
+
raw = YAML.load_file(config_path)
|
28
|
+
return { } unless Hash === raw
|
29
|
+
|
30
|
+
translate_raw_to_config(raw)
|
31
|
+
end
|
32
|
+
|
33
|
+
def save(config)
|
34
|
+
raw = translate_config_to_raw(config)
|
35
|
+
|
36
|
+
if config.empty?
|
37
|
+
File.delete(config_path) if File.file?(config_path)
|
38
|
+
else
|
39
|
+
config_dir = File.dirname(config_path)
|
40
|
+
FileUtils.mkpath(config_dir) unless File.directory?(config_dir)
|
41
|
+
File.open(config_path, "wb"){|f| YAML.dump(raw, f)}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require "librarian/source"
|
2
|
+
|
3
|
+
module Librarian
|
4
|
+
module Config
|
5
|
+
class HashSource < Source
|
6
|
+
|
7
|
+
attr_accessor :name, :raw
|
8
|
+
private :name=, :raw=
|
9
|
+
|
10
|
+
def initialize(adapter_name, options = { })
|
11
|
+
super
|
12
|
+
|
13
|
+
self.name = options.delete(:name) or raise ArgumentError, "must provide name"
|
14
|
+
self.raw = options.delete(:raw) or raise ArgumentError, "must provide raw"
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
name
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def load
|
24
|
+
translate_raw_to_config(raw)
|
25
|
+
end
|
26
|
+
|
27
|
+
def save(config)
|
28
|
+
raise Error, "nonsense!"
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
require "librarian/error"
|
2
|
+
|
3
|
+
module Librarian
|
4
|
+
module Config
|
5
|
+
class Source
|
6
|
+
|
7
|
+
RAW_KEY_SUFFIX_VALIDITY_PATTERN =
|
8
|
+
/\A[A-Z0-9_]+\z/
|
9
|
+
CONFIG_KEY_VALIDITY_PATTERN =
|
10
|
+
/\A[a-z][a-z0-9\-]+(?:\.[a-z0-9\-]+)*\z/
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def raw_key_suffix_validity_pattern
|
14
|
+
RAW_KEY_SUFFIX_VALIDITY_PATTERN
|
15
|
+
end
|
16
|
+
def config_key_validity_pattern
|
17
|
+
CONFIG_KEY_VALIDITY_PATTERN
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_accessor :adapter_name
|
22
|
+
private :adapter_name=
|
23
|
+
|
24
|
+
def initialize(adapter_name, options = { })
|
25
|
+
self.adapter_name = adapter_name
|
26
|
+
|
27
|
+
self.forbidden_keys = options.delete(:forbidden_keys) || []
|
28
|
+
end
|
29
|
+
|
30
|
+
def [](key)
|
31
|
+
load!
|
32
|
+
|
33
|
+
data[key]
|
34
|
+
end
|
35
|
+
|
36
|
+
def []=(key, value)
|
37
|
+
key_permitted?(key) or raise Error, "key not permitted: #{key.inspect}"
|
38
|
+
value_permitted?(key, value) or raise Error, "value for key #{key.inspect} not permitted: #{value.inspect}"
|
39
|
+
|
40
|
+
load!
|
41
|
+
if value.nil?
|
42
|
+
data.delete(key)
|
43
|
+
else
|
44
|
+
data[key] = value
|
45
|
+
end
|
46
|
+
save(data)
|
47
|
+
end
|
48
|
+
|
49
|
+
def keys
|
50
|
+
load!
|
51
|
+
|
52
|
+
data.keys
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
attr_accessor :data, :forbidden_keys
|
58
|
+
|
59
|
+
def load!
|
60
|
+
self.data = load unless data
|
61
|
+
end
|
62
|
+
|
63
|
+
def key_permitted?(key)
|
64
|
+
String === key &&
|
65
|
+
config_key_validity_pattern === key &&
|
66
|
+
!forbidden_keys.any?{|k| k === key}
|
67
|
+
end
|
68
|
+
|
69
|
+
def value_permitted?(key, value)
|
70
|
+
return true if value.nil?
|
71
|
+
|
72
|
+
String === value
|
73
|
+
end
|
74
|
+
|
75
|
+
def raw_key_valid?(key)
|
76
|
+
return false unless key.start_with?(raw_key_prefix)
|
77
|
+
|
78
|
+
suffix = key[raw_key_prefix.size..-1]
|
79
|
+
raw_key_suffix_validity_pattern =~ suffix
|
80
|
+
end
|
81
|
+
|
82
|
+
def raw_key_suffix_validity_pattern
|
83
|
+
self.class.raw_key_suffix_validity_pattern
|
84
|
+
end
|
85
|
+
|
86
|
+
def config_key_valid?(key)
|
87
|
+
config_key_validity_pattern === key
|
88
|
+
end
|
89
|
+
|
90
|
+
def config_key_validity_pattern
|
91
|
+
self.class.config_key_validity_pattern
|
92
|
+
end
|
93
|
+
|
94
|
+
def raw_key_prefix
|
95
|
+
@key_prefix ||= "LIBRARIAN_#{adapter_name.upcase}_"
|
96
|
+
end
|
97
|
+
|
98
|
+
def assert_raw_keys_valid!(raw)
|
99
|
+
bad_keys = raw.keys.reject{|k| raw_key_valid?(k)}
|
100
|
+
unless bad_keys.empty?
|
101
|
+
config_path_s = config_path.to_s.inspect
|
102
|
+
bad_keys_s = bad_keys.map(&:inspect).join(", ")
|
103
|
+
raise Error, "config #{to_s} has bad keys: #{bad_keys_s}"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def assert_config_keys_valid!(config)
|
108
|
+
bad_keys = config.keys.reject{|k| config_key_valid?(k)}
|
109
|
+
unless bad_keys.empty?
|
110
|
+
bad_keys_s = bad_keys.map(&:inspect).join(", ")
|
111
|
+
raise Error, "config has bad keys: #{bad_keys_s}"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def assert_values_valid!(data)
|
116
|
+
bad_data = data.reject{|k, v| String === v}
|
117
|
+
bad_keys = bad_data.keys
|
118
|
+
|
119
|
+
unless bad_keys.empty?
|
120
|
+
bad_keys_s = bad_keys.map(&:inspect).join(", ")
|
121
|
+
raise Error, "config has bad values for keys: #{bad_keys_s}"
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def translate_raw_to_config(raw)
|
126
|
+
assert_raw_keys_valid!(raw)
|
127
|
+
assert_values_valid!(raw)
|
128
|
+
|
129
|
+
Hash[raw.map do |key, value|
|
130
|
+
key = key[raw_key_prefix.size .. -1]
|
131
|
+
key = key.downcase.gsub(/__/, ".").gsub(/_/, "-")
|
132
|
+
[key, value]
|
133
|
+
end]
|
134
|
+
end
|
135
|
+
|
136
|
+
def translate_config_to_raw(config)
|
137
|
+
assert_config_keys_valid!(config)
|
138
|
+
assert_values_valid!(config)
|
139
|
+
|
140
|
+
Hash[config.map do |key, value|
|
141
|
+
key = key.gsub(/\./, "__").gsub(/\-/, "_").upcase
|
142
|
+
key = "#{raw_key_prefix}#{key}"
|
143
|
+
[key, value]
|
144
|
+
end]
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|