tty-config 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 55d72fde5f6fbfded2c40705454c4d3cb9b92b5c
4
+ data.tar.gz: 5dc7c64fe76b97427b8b62141a64d3ac34faf948
5
+ SHA512:
6
+ metadata.gz: 1076327953cebbfae39402c5c3fcb61049c7299be02903628442f8eaf8dc2c4ca1d1b8d1515bc627373fc382ea22c7f7389fe2655b06993612a8e6e0cea794e2
7
+ data.tar.gz: 9ee8ed6258b9eb4cd319068ef227af4f4a8bfc5bb168308d0cf193849bcc5556ae3debdcd57a5fef49250ee106303c9779d8d5c35a78ee296f5ddc5476a3892e
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /Gemfile.lock
3
+ /.yardoc
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ # rspec failure tracking
12
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,23 @@
1
+ ---
2
+ language: ruby
3
+ sudo: false
4
+ cache: bundler
5
+ before_install: "gem update bundler"
6
+ script: "bundle exec rake ci"
7
+ rvm:
8
+ - 2.0.0
9
+ - 2.1.10
10
+ - 2.2.8
11
+ - 2.3.6
12
+ - 2.4.3
13
+ - 2.5.0
14
+ - ruby-head
15
+ - jruby-9000
16
+ - jruby-head
17
+ matrix:
18
+ allow_failures:
19
+ - rvm: ruby-head
20
+ - rvm: jruby-head
21
+ fast_finish: true
22
+ branches:
23
+ only: master
data/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # Change log
2
+
3
+ ## [v0.1.0] - 2018-04-14
4
+
5
+ * Initial implementation and release
6
+
7
+ [v0.1.0]: https://github.com/piotrmurach/tty-markdown/compare/v0.1.0
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at [email]. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ gemspec
6
+
7
+ group :test do
8
+ gem 'benchmark-ips', '~> 2.7.2'
9
+ gem 'simplecov', '~> 0.14.1'
10
+ gem 'coveralls', '~> 0.8.21'
11
+ end
12
+
13
+ group :metrics do
14
+ gem 'yard', '~> 0.9.12'
15
+ gem 'yardstick', '~> 0.9.9'
16
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Piotr Murach
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,407 @@
1
+ # TTY::Config [![Gitter](https://badges.gitter.im/Join%20Chat.svg)][gitter]
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/tty-config.svg)][gem]
4
+ [![Build Status](https://secure.travis-ci.org/piotrmurach/tty-config.svg?branch=master)][travis]
5
+ [![Build status](https://ci.appveyor.com/api/projects/status/2383i0dn3hlw9cnn?svg=true)][appveyor]
6
+ [![Maintainability](https://api.codeclimate.com/v1/badges/dfac05073e1549e9dbb6/maintainability)][codeclimate]
7
+ [![Coverage Status](https://coveralls.io/repos/github/piotrmurach/tty-config/badge.svg)][coverage]
8
+ [![Inline docs](http://inch-ci.org/github/piotrmurach/tty-config.svg?branch=master)][inchpages]
9
+
10
+ [gitter]: https://gitter.im/piotrmurach/tty
11
+ [gem]: http://badge.fury.io/rb/tty-config
12
+ [travis]: http://travis-ci.org/piotrmurach/tty-config
13
+ [appveyor]: https://ci.appveyor.com/project/piotrmurach/tty-config
14
+ [codeclimate]: https://codeclimate.com/github/piotrmurach/tty-config/maintainability
15
+ [coverage]: https://coveralls.io/github/piotrmurach/tty-config
16
+ [inchpages]: http://inch-ci.org/github/piotrmurach/tty-config
17
+
18
+ > Define, read and write any Ruby app configurations with a penchant for terminal clients.
19
+
20
+ **TTY::Config** provides app configuration component for [TTY](https://github.com/piotrmurach/tty) toolkit.
21
+
22
+ ## Features
23
+
24
+ * Read & write configurations in YAML, JSON, TOML formats
25
+ * Simple interface for setting and fetching values for deeply nested keys
26
+ * Merging of configuration options from other hashes
27
+
28
+ ## Installation
29
+
30
+ Add this line to your application's Gemfile:
31
+
32
+ ```ruby
33
+ gem 'tty-config'
34
+ ```
35
+
36
+ And then execute:
37
+
38
+ $ bundle
39
+
40
+ Or install it yourself as:
41
+
42
+ $ gem install tty-config
43
+
44
+ ## Contents
45
+
46
+ * [1. Usage](#1-usage)
47
+ * [2. Interface](#2-interface)
48
+ * [2.1 set](#21-set)
49
+ * [2.2 set_if_empty](#22-set_if_empty)
50
+ * [2.3 fetch](#23-fetch)
51
+ * [2.4 merge](#24-merge)
52
+ * [2.5 coerce](#25-coerce)
53
+ * [2.6 append](#26-append)
54
+ * [2.7 remove](#27-remove)
55
+ * [2.8 delete](#28-delete)
56
+ * [2.9 filename=](#29-filename)
57
+ * [2.10 extname=](#210-extname)
58
+ * [2.11 append_path](#211-append_path)
59
+ * [2.12 prepend_path](#212-prepend_path)
60
+ * [2.13 read](#213-read)
61
+ * [2.14 write](#214-write)
62
+ * [2.15 persisted?](#215-persisted)
63
+
64
+ ## 1. Usage
65
+
66
+ Initialize the configuration and provide the name:
67
+
68
+ ```ruby
69
+ config = TTY::Config.new
70
+ config.filename = 'investments'
71
+ ```
72
+
73
+ then configure values for different nested keys with `set` and `append`:
74
+
75
+ ```ruby
76
+ config.set(:settings, :base, value: 'USD')
77
+ config.set(:settings, :color, value: true)
78
+ config.set(:coins, value: ['BTC'])
79
+
80
+ config.append('ETH', 'TRX', 'DASH', to: :coins)
81
+ ```
82
+
83
+ get any value by using `fetch`:
84
+
85
+ ```ruby
86
+ config.fetch(:settings, :base)
87
+ # => 'USD'
88
+
89
+ config.fetch(:coins)
90
+ # => ['BTC', 'ETH', 'TRX', 'DASH']
91
+ ```
92
+
93
+ and `write` configration out to `investments.yml`:
94
+
95
+ ```ruby
96
+ config.write
97
+ # =>
98
+ # ---
99
+ # settings:
100
+ # base: USD
101
+ # color: true
102
+ # coins:
103
+ # - BTC
104
+ # - ETH
105
+ # - TRX
106
+ # - DASH
107
+ ```
108
+
109
+ and then to read an `investments.yml` file, you need to provide the locations to search in:
110
+
111
+ ```ruby
112
+ config.append_path Dir.pwd
113
+ config.append_path Dir.home
114
+ ```
115
+
116
+ Finally, read in configuration back again:
117
+
118
+ ```ruby
119
+ config.read
120
+ ```
121
+
122
+ ## 2. Interface
123
+
124
+ ### 2.1 set
125
+
126
+ To set configuration setting use `set` method. It accepts any number of keys and value by either using `:value` keyword argument or passing a block:
127
+
128
+ ```ruby
129
+ config.set(:base, value: 'USD')
130
+ config.set(:base) { 'USD' }
131
+ ```
132
+
133
+ The block version of specifying a value will mean that the value is evaluated every time its being read.
134
+
135
+ You can also specify deeply nested configuration settings by passing sequence of keys:
136
+
137
+ ```ruby
138
+ config.set :settings, :base, value: 'USD'
139
+ ```
140
+
141
+ is equivalent to:
142
+
143
+ ```ruby
144
+ config.set 'settings.base', value: 'USD'
145
+ ```
146
+
147
+ Internally all configuration settings are stored as string keys for ease of working with configuration files and command line application's inputs.
148
+
149
+ ### 2.2 set_if_empty
150
+
151
+ To set a configuration setting only if it hasn't been set before use `set_if_empty`:
152
+
153
+ ```ruby
154
+ config.set_if_empty :base, value: 'USD'
155
+ ```
156
+
157
+ Similar to `set` it allows you to specify arbitrary sequence of keys followed by a key value or block:
158
+
159
+ ```ruby
160
+ config.set_if_empty :settings, :base, value: 'USD'
161
+ ```
162
+
163
+ ### 2.3 fetch
164
+
165
+ To get a configuration setting use `fetch`, which can accept default value either with a `:default` keyword or a block that will be lazy evaluated:
166
+
167
+ ```ruby
168
+ config.fetch(:base, default: 'USD')
169
+ config.fetch(:base) { 'USD' }
170
+ ```
171
+
172
+ Similar to `set` operation, `fetch` allows you to retrieve deeply nested values:
173
+
174
+ ```ruby
175
+ config.fetch(:settings, :base) # => USD
176
+ ```
177
+
178
+ is equivalent to:
179
+
180
+ ```ruby
181
+ config.fetch('settings.base')
182
+ ```
183
+
184
+ `fetch` has indifferent access so you can mix string and symbol keys, all the following examples retrieve the value:
185
+
186
+ ```ruby
187
+ config.fetch(:settings, :base)
188
+ config.fetch('settings', 'base')
189
+ config.fetch(:settings', 'base')
190
+ config.fetch('settings', :base)
191
+ ```
192
+
193
+ ### 2.4 merge
194
+
195
+ To merge in other configuration settings as hash use `merge`:
196
+
197
+ ```ruby
198
+ config.set(:a, :b, value: 1)
199
+ config.set(:a, :c, value: 2)
200
+
201
+ config.merge({'a' => {'c' => 3, 'd' => 4}})
202
+
203
+ config.fetch(:a, :c) # => 3
204
+ config.fetch(:a, :d) # => 4
205
+ ```
206
+
207
+ Internally all configuration settings are stored as string keys for ease of working with file values and command line applications inputs.
208
+
209
+ ### 2.5 coerce
210
+
211
+ You can initialize configuration based on a hash, with all the keys converted to symbols:
212
+
213
+ ```ruby
214
+ hash = {"settings" => {"base" => "USD", "exchange" => "CCCAGG"}}
215
+ config = TTY::Config.coerce(hash)
216
+ config.to_h
217
+ # =>
218
+ # {settings: {base: "USD", exchange: "CCCAGG"}}
219
+ ```
220
+
221
+ ### 2.6 append
222
+
223
+ To append arbitrary number of values to a value under a given key use `append`:
224
+
225
+ ```ruby
226
+ config.set(:coins) { ["BTC"] }
227
+
228
+ config.append("ETH", "TRX", to: :coins)
229
+ # =>
230
+ # {coins: ["BTC", "ETH", "TRX"]}
231
+ ```
232
+
233
+ You can also append values to deeply nested keys:
234
+
235
+ ```ruby
236
+ config.set(:settings, :bases, value: ["USD"])
237
+
238
+ config.append("EUR", "GBP", to: [:settings, :bases])
239
+ # =>
240
+ # {settings: {bases: ["USD", "EUR", "GBP"]}}
241
+ ```
242
+
243
+ ### 2.7 remove
244
+
245
+ Use `remove` to remove a set of values from a key.
246
+
247
+ ```ruby
248
+ config.set(:coins, value: ["BTC", "TRX", "ETH", "DASH"])
249
+
250
+ config.remove("TRX", "DASH", from: :coins)
251
+ # =>
252
+ # ["BTC", "ETH"]
253
+ ```
254
+
255
+ If the key is nested the `:from` accepts an array:
256
+
257
+ ```ruby
258
+ config.set(:holdings, :coins, value: ["BTC", "TRX", "ETH", "DASH"])
259
+
260
+ config.remove("TRX", "DASH", from: [:holdings, :coins])
261
+ # =>
262
+ # ["BTC", "ETH"]
263
+ ```
264
+
265
+ ### 2.8 delete
266
+
267
+ To completely delete a value and corresponding key use `delete`:
268
+
269
+ ```ruby
270
+ config.set(:base, "USD")
271
+ config.delete(:base)
272
+ # =>
273
+ # "USD"
274
+ ```
275
+
276
+ You can also delete deeply nested keys and their values:
277
+
278
+ ```ruby
279
+ config.set(:settings, :base, "USD")
280
+ config.delete(:settings, :base)
281
+ # =>
282
+ # "USD"
283
+ ```
284
+
285
+ ### 2.9 filename=
286
+
287
+ By default, **TTY::Config** searches for `config` named configuration file. To change this use `filename=` method without the extension name:
288
+
289
+ ```ruby
290
+ config.filename = 'investments'
291
+ ```
292
+
293
+ Then any supported extensions will be search for such as `.yml`, `.json` and `.toml`.
294
+
295
+ ### 2.10 extname=
296
+
297
+ By default '.yml' extension is used to write configuration out to a file but you can change that with `extname=`:
298
+
299
+ ```ruby
300
+ config.extname = '.toml'
301
+ ```
302
+
303
+ ### 2.11 append_path
304
+
305
+ You need to tell the **TTY::Config** where to search for configuration files. To search multiple paths for a configuration file use `append_path` or `prepend_path` methods.
306
+
307
+ For example, if you want to search through `/etc` directory first, then user home directory and then current directory do:
308
+
309
+ ```ruby
310
+ config.append_path("/etc/") # look in /etc directory
311
+ config.append_path(Dir.home) # look in user's home directory
312
+ config.append_path(Dir.pwd) # look in current working directory
313
+ ```
314
+
315
+ None of these paths are required, but you should provide at least one path if you wish to read configuration file.
316
+
317
+ ### 2.12 prepend_path
318
+
319
+ The `prepend_path` allows you to add configuration search paths that should be searched first.
320
+
321
+ ```ruby
322
+ config.append_path(Dir.pwd) # look in current working directory second
323
+ config.prepend_path(Dir.home) # look in user's home directory first
324
+ ```
325
+
326
+ ### 2.13 read
327
+
328
+ There are two ways for reading configuration files and both use the `read` method.
329
+
330
+ First one, searches through provided locations to find configuration file and read it. Therefore, you need to specify at least one search path that contains the configuration file.
331
+
332
+ ```ruby
333
+ config.append_path(Dir.pwd) # look in current working directory
334
+ config.filename = 'investments' # file to search for
335
+ ```
336
+
337
+ Find and read the configuration file:
338
+
339
+ ```ruby
340
+ config.read
341
+ ```
342
+
343
+ However, you can also specify directly the file to read without setting up any search paths or filenames:
344
+
345
+ ```ruby
346
+ config.read('./investments.toml')
347
+ ```
348
+
349
+ ### 2.14 write
350
+
351
+ By default **TTY::Config**, persists configuration file in the current working directory with a `config.yml` name. However, you can change that by specifying the filename and extension type:
352
+
353
+ ```ruby
354
+ config.filename = 'investments'
355
+ config.extname = '.toml'
356
+ ```
357
+
358
+ To write current configuration to a file, you can either specified direct location path and filename:
359
+
360
+ ```ruby
361
+ config.write('./investments.toml')
362
+ ```
363
+
364
+ Or, specify location paths to be searched for already existing configuration to overwrite:
365
+
366
+ ```ruby
367
+ config.append_path(Dir.pwd) # search current working directory
368
+
369
+ config.write
370
+ ```
371
+
372
+ To create configuration file regardless whether it exists or not, use `:force` flag:
373
+
374
+ ```ruby
375
+ config.write(force: true) # overwrite any found config file
376
+ config.write('./investments.toml', force: true) # overwrite specific config file
377
+ ```
378
+
379
+ ### 2.15 persisted?
380
+
381
+ To check if a configuration file exists within the configured search paths use `persisted?` method:
382
+
383
+ ```ruby
384
+ config.persisted? # => true
385
+ ```
386
+
387
+ ## Development
388
+
389
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
390
+
391
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
392
+
393
+ ## Contributing
394
+
395
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/tty-config. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
396
+
397
+ ## License
398
+
399
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
400
+
401
+ ## Code of Conduct
402
+
403
+ Everyone interacting in the Tty::Config project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/piotrmurach/tty-config/blob/master/CODE_OF_CONDUCT.md).
404
+
405
+ ## Copyright
406
+
407
+ Copyright (c) 2018 Piotr Murach. See LICENSE for further details.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ FileList['tasks/**/*.rake'].each(&method(:import))
4
+
5
+ desc 'Run all specs'
6
+ task ci: %w[ spec ]
7
+
8
+ task default: :spec
data/appveyor.yml ADDED
@@ -0,0 +1,23 @@
1
+ ---
2
+ install:
3
+ - SET PATH=C:\Ruby%ruby_version%\bin;%PATH%
4
+ - ruby --version
5
+ - gem --version
6
+ - bundle install
7
+ build: off
8
+ test_script:
9
+ - bundle exec rake ci
10
+ environment:
11
+ matrix:
12
+ - ruby_version: "200"
13
+ - ruby_version: "200-x64"
14
+ - ruby_version: "21"
15
+ - ruby_version: "21-x64"
16
+ - ruby_version: "22"
17
+ - ruby_version: "22-x64"
18
+ - ruby_version: "23"
19
+ - ruby_version: "23-x64"
20
+ - ruby_version: "24"
21
+ - ruby_version: "24-x64"
22
+ - ruby_version: "25"
23
+ - ruby_version: "25-x64"
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "tty/config"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ 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
data/lib/tty-config.rb ADDED
@@ -0,0 +1 @@
1
+ require_relative 'tty/config'
data/lib/tty/config.rb ADDED
@@ -0,0 +1,408 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ require_relative 'config/version'
6
+
7
+ module TTY
8
+ class Config
9
+ # Error raised when key fails validation
10
+ ReadError = Class.new(StandardError)
11
+ # Error raised when issues writing configuration to a file
12
+ WriteError = Class.new(StandardError)
13
+ # Erorrr raised when setting unknown file extension
14
+ UnsupportedExtError = Class.new(StandardError)
15
+
16
+ def self.coerce(hash, &block)
17
+ new(normalize_hash(hash), &block)
18
+ end
19
+
20
+ # Convert string keys via method
21
+ #
22
+ # @api private
23
+ def self.normalize_hash(hash, method = :to_sym)
24
+ hash.reduce({}) do |acc, (key, val)|
25
+ value = val.is_a?(::Hash) ? normalize_hash(val, method) : val
26
+ acc[key.public_send(method)] = value
27
+ acc
28
+ end
29
+ end
30
+
31
+ # A collection of config paths
32
+ # @api public
33
+ attr_reader :location_paths
34
+
35
+ # The key delimiter used for specifying deeply nested keys
36
+ # @api public
37
+ attr_reader :key_delim
38
+
39
+ # The name of the configuration file without extension
40
+ # @api public
41
+ attr_accessor :filename
42
+
43
+ # The name of the configuration file extension
44
+ # @api public
45
+ attr_reader :extname
46
+
47
+ def initialize(settings = {})
48
+ @location_paths = []
49
+ @settings = settings
50
+ @validators = {}
51
+ @filename = 'config'
52
+ @extname = '.yml'
53
+ @extensions = ['.yaml', '.yml', '.json', '.toml']
54
+ @key_delim = '.'
55
+
56
+ yield(self) if block_given?
57
+ end
58
+
59
+ # Set extension name
60
+ #
61
+ # @raise [TTY::Config::UnsupportedExtError]
62
+ #
63
+ # api public
64
+ def extname=(name)
65
+ unless @extensions.include?(name)
66
+ raise UnsupportedExtError, "Config file format `#{name}` is not supported."
67
+ end
68
+ @extname = name
69
+ end
70
+
71
+ # Add path to locations to search in
72
+ #
73
+ # @api public
74
+ def append_path(path)
75
+ @location_paths << path
76
+ end
77
+
78
+ # Insert location path at the begining
79
+ #
80
+ # @api public
81
+ def prepend_path(path)
82
+ @location_paths.unshift(path)
83
+ end
84
+
85
+ # Set a value for a composite key and overrides any existing keys.
86
+ # Keys are case-insensitive
87
+ #
88
+ # @api public
89
+ def set(*keys, value: nil, &block)
90
+ assert_either_value_or_block(value, block)
91
+ keys = convert_to_keys(keys)
92
+
93
+ deepest_setting = deep_set(@settings, *keys[0...-1])
94
+ deepest_setting[keys.last] = block || value
95
+ deepest_setting[keys.last]
96
+ end
97
+
98
+ # Set a value for a composite key if not present already
99
+ #
100
+ # @param [Array[String|Symbol]] keys
101
+ # the keys to set value for
102
+ #
103
+ # @api public
104
+ def set_if_empty(*keys, value: nil, &block)
105
+ return unless deep_find(@settings, keys.last.to_s).nil?
106
+ block ? set(*keys, &block) : set(*keys, value: value)
107
+ end
108
+
109
+ # Fetch value under a composite key
110
+ #
111
+ # @param [Array[String|Symbol]] keys
112
+ # the keys to get value at
113
+ # @param [Object] default
114
+ #
115
+ # @api public
116
+ def fetch(*keys, default: nil, &block)
117
+ keys = convert_to_keys(keys)
118
+ value = deep_fetch(@settings, *keys)
119
+ value = block || default if value.nil?
120
+ while callable_without_params?(value)
121
+ value = value.call
122
+ end
123
+ value
124
+ end
125
+
126
+ # Merge in other configuration settings
127
+ #
128
+ # @param [Hash[Object]] other_settings
129
+ #
130
+ # @api public
131
+ def merge(other_settings)
132
+ @settings = deep_merge(@settings, other_settings)
133
+ end
134
+
135
+ # Append values to an already existing nested key
136
+ #
137
+ # @param [Array[String|Symbol]] values
138
+ # the values to append
139
+ #
140
+ # @api public
141
+ def append(*values, to: nil)
142
+ keys = Array(to)
143
+ set(*keys, value: Array(fetch(*keys)) + values)
144
+ end
145
+
146
+ # Remove a set of values from a nested key
147
+ #
148
+ # @param [Array[String|Symbol]] keys
149
+ # the keys for a value removal
150
+ #
151
+ # @api public
152
+ def remove(*values, from: nil)
153
+ keys = Array(from)
154
+ set(*keys, value: Array(fetch(*keys)) - values)
155
+ end
156
+
157
+ # Delete a value from a nested key
158
+ #
159
+ # @param [Array[String|Symbol]] keys
160
+ # the keys for a value deletion
161
+ #
162
+ # @api public
163
+ def delete(*keys)
164
+ keys = convert_to_keys(keys)
165
+ deep_delete(*keys, @settings)
166
+ end
167
+
168
+ # @api private
169
+ def find_file
170
+ @location_paths.each do |location_path|
171
+ path = search_in_path(location_path)
172
+ return path if path
173
+ end
174
+ nil
175
+ end
176
+ alias source_file find_file
177
+
178
+ # Check if configuration file exists
179
+ #
180
+ # @return [Boolean]
181
+ #
182
+ # @api public
183
+ def persisted?
184
+ !find_file.nil?
185
+ end
186
+
187
+ # Find and read a configuration file.
188
+ #
189
+ # If the file doesn't exist or if there is an error loading it
190
+ # the TTY::Config::ReadError will be raised.
191
+ #
192
+ # @param [String] file
193
+ # the path to the configuration file to be read
194
+ #
195
+ # @raise [TTY::Config::ReadError]
196
+ #
197
+ # @api public
198
+ def read(file = find_file)
199
+ if file.nil?
200
+ raise ReadError, "No file found to read configuration from!"
201
+ elsif !::File.exist?(file)
202
+ raise ReadError, "Configuration file `#{file}` does not exist!"
203
+ end
204
+
205
+ merge(unmarshal(file))
206
+ end
207
+
208
+ # Write current configuration to a file.
209
+ #
210
+ # @param [String] file
211
+ # the path to a file
212
+ #
213
+ # @api public
214
+ def write(file = find_file, force: false)
215
+ if file && ::File.exist?(file)
216
+ if !force
217
+ raise WriteError, "File `#{file}` already exists. " \
218
+ "Use :force option to overwrite."
219
+ elsif !::File.writable?(file)
220
+ raise WriteError, "Cannot write to #{file}."
221
+ end
222
+ end
223
+
224
+ if file.nil?
225
+ dir = @location_paths.empty? ? Dir.pwd : @location_paths.first
226
+ file = ::File.join(dir, "#{filename}#{@extname}")
227
+ end
228
+
229
+ marshal(file, @settings)
230
+ end
231
+
232
+ # Current configuration
233
+ #
234
+ # @api public
235
+ def to_hash
236
+ @settings.dup
237
+ end
238
+ alias to_h to_hash
239
+
240
+ private
241
+
242
+ def callable_without_params?(object)
243
+ object.respond_to?(:call) &&
244
+ (!object.respond_to?(:arity) || object.arity.zero?)
245
+ end
246
+
247
+ def assert_either_value_or_block(value, block)
248
+ return if value.nil? || block.nil?
249
+ raise ArgumentError, "Can't set both value and block"
250
+ end
251
+
252
+ # Set value under deeply nested keys
253
+ #
254
+ # The scan starts with the top level key and follows
255
+ # a sequence of keys. In case where intermediate keys do
256
+ # not exist, a new hash is created.
257
+ #
258
+ # @param [Hash] settings
259
+ #
260
+ # @param [Array[Object]]
261
+ # the keys to nest
262
+ #
263
+ # @api private
264
+ def deep_set(settings, *keys)
265
+ return settings if keys.empty?
266
+ key, *rest = *keys
267
+ value = settings[key]
268
+
269
+ if value.nil? && rest.empty?
270
+ settings[key] = {}
271
+ elsif value.nil? && !rest.empty?
272
+ settings[key] = {}
273
+ deep_set(settings[key], *rest)
274
+ else # nested hash value present
275
+ settings[key] = value
276
+ deep_set(settings[key], *rest)
277
+ end
278
+ end
279
+
280
+ def deep_find(settings, key, found = nil)
281
+ if settings.respond_to?(:key?) && settings.key?(key)
282
+ settings[key]
283
+ elsif settings.is_a?(Enumerable)
284
+ settings.each { |obj| found = deep_find(obj, key) }
285
+ found
286
+ end
287
+ end
288
+
289
+ def convert_to_keys(keys)
290
+ first_key = keys[0]
291
+ if first_key.to_s.include?(key_delim)
292
+ first_key.split(key_delim)
293
+ else
294
+ keys.map(&:to_s)
295
+ end
296
+ end
297
+
298
+ # Fetch value under deeply nested keys with indiffernt key access
299
+ #
300
+ # @param [Hash] settings
301
+ #
302
+ # @param [Array[Object]] keys
303
+ #
304
+ # @api private
305
+ def deep_fetch(settings, *keys)
306
+ key, *rest = keys
307
+ value = settings.fetch(key.to_s, settings[key.to_sym])
308
+ if value.nil? || rest.empty?
309
+ value
310
+ else
311
+ deep_fetch(value, *rest)
312
+ end
313
+ end
314
+
315
+ # @api private
316
+ def deep_merge(this_hash, other_hash, &block)
317
+ this_hash.merge(other_hash) do |key, this_val, other_val|
318
+ if this_val.is_a?(::Hash) && other_val.is_a?(::Hash)
319
+ deep_merge(this_val, other_val, &block)
320
+ elsif block_given?
321
+ block[key, this_val, other_val]
322
+ else
323
+ other_val
324
+ end
325
+ end
326
+ end
327
+
328
+ # @api private
329
+ def deep_delete(*keys, settings)
330
+ key, *rest = keys
331
+ value = settings[key]
332
+ if !value.nil? && value.is_a?(::Hash)
333
+ deep_delete(*rest, value)
334
+ elsif !value.nil?
335
+ settings.delete(key)
336
+ end
337
+ end
338
+
339
+ # @api private
340
+ def search_in_path(path)
341
+ path = Pathname.new(path)
342
+ @extensions.each do |ext|
343
+ if ::File.exist?(path.join("#{filename}#{ext}").to_s)
344
+ return path.join("#{filename}#{ext}").to_s
345
+ end
346
+ end
347
+ nil
348
+ end
349
+
350
+ # @api private
351
+ def unmarshal(file)
352
+ ext = ::File.extname(file)
353
+ self.extname = ext
354
+ self.filename = ::File.basename(file, ext)
355
+ gem_name = nil
356
+
357
+ case ext
358
+ when '.yaml', '.yml'
359
+ require 'yaml'
360
+ if YAML.respond_to?(:safe_load)
361
+ YAML.safe_load(File.read(file))
362
+ else
363
+ YAML.load(File.read(file))
364
+ end
365
+ when '.json'
366
+ require 'json'
367
+ JSON.parse(File.read(file))
368
+ when '.toml'
369
+ gem_name = 'toml'
370
+ require 'toml'
371
+ TOML.load(::File.read(file))
372
+ else
373
+ raise ReadError, "Config file format `#{ext}` is not supported."
374
+ end
375
+ rescue LoadError
376
+ puts "Please install `#{gem_name}`"
377
+ raise ReadError, "Gem `#{gem_name}` is missing. Please install it " \
378
+ "to read #{ext} configuration format."
379
+ end
380
+
381
+ # @api private
382
+ def marshal(file, data)
383
+ ext = ::File.extname(file)
384
+ self.extname = ext
385
+ self.filename = ::File.basename(file, ext)
386
+ gem_name = nil
387
+
388
+ case ext
389
+ when '.yaml', '.yml'
390
+ require 'yaml'
391
+ ::File.write(file, YAML.dump(self.class.normalize_hash(data, :to_s)))
392
+ when '.json'
393
+ require 'json'
394
+ ::File.write(file, JSON.pretty_generate(data))
395
+ when '.toml'
396
+ gem_name = 'toml'
397
+ require 'toml'
398
+ ::File.write(file, TOML::Generator.new(data).body)
399
+ else
400
+ raise WriteError, "Config file format `#{ext}` is not supported."
401
+ end
402
+ rescue LoadError
403
+ puts "Please install `#{gem_name}`"
404
+ raise ReadError, "Gem `#{gem_name}` is missing. Please install it " \
405
+ "to read #{ext} configuration format."
406
+ end
407
+ end # Config
408
+ end # TTY
@@ -0,0 +1,5 @@
1
+ module TTY
2
+ class Config
3
+ VERSION = "0.1.0".freeze
4
+ end
5
+ end
@@ -0,0 +1,11 @@
1
+ # encoding: utf-8
2
+
3
+ desc 'Load gem inside irb console'
4
+ task :console do
5
+ require 'irb'
6
+ require 'irb/completion'
7
+ require File.join(__FILE__, '../../lib/tty-config')
8
+ ARGV.clear
9
+ IRB.start
10
+ end
11
+ task c: %w[ console ]
@@ -0,0 +1,11 @@
1
+ # encoding: utf-8
2
+
3
+ desc 'Measure code coverage'
4
+ task :coverage do
5
+ begin
6
+ original, ENV['COVERAGE'] = ENV['COVERAGE'], 'true'
7
+ Rake::Task['spec'].invoke
8
+ ensure
9
+ ENV['COVERAGE'] = original
10
+ end
11
+ end
data/tasks/spec.rake ADDED
@@ -0,0 +1,29 @@
1
+ # encoding: utf-8
2
+
3
+ begin
4
+ require 'rspec/core/rake_task'
5
+
6
+ desc 'Run all specs'
7
+ RSpec::Core::RakeTask.new(:spec) do |task|
8
+ task.pattern = 'spec/{unit,integration}{,/*/**}/*_spec.rb'
9
+ end
10
+
11
+ namespace :spec do
12
+ desc 'Run unit specs'
13
+ RSpec::Core::RakeTask.new(:unit) do |task|
14
+ task.pattern = 'spec/unit{,/*/**}/*_spec.rb'
15
+ end
16
+
17
+ desc 'Run integration specs'
18
+ RSpec::Core::RakeTask.new(:integration) do |task|
19
+ task.pattern = 'spec/integration{,/*/**}/*_spec.rb'
20
+ end
21
+ end
22
+
23
+ rescue LoadError
24
+ %w[spec spec:unit spec:integration].each do |name|
25
+ task name do
26
+ $stderr.puts "In order to run #{name}, do `gem install rspec`"
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "tty/config/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "tty-config"
7
+ spec.version = TTY::Config::VERSION
8
+ spec.authors = ["Piotr Murach"]
9
+ spec.email = [""]
10
+
11
+ spec.summary = %q{Define, read and write any Ruby app configurations with a penchant for terminal clients.}
12
+ spec.description = %q{Define, read and write any Ruby app configurations with a penchant for terminal clients.}
13
+ spec.homepage = "https://piotrmurach.github.io/tty"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(test|spec|features)/})
18
+ end
19
+ spec.bindir = "exe"
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.required_ruby_version = '>= 2.0.0'
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.16"
26
+ spec.add_development_dependency "rake", "~> 10.0"
27
+ spec.add_development_dependency "rspec", "~> 3.0"
28
+ spec.add_development_dependency "toml", "~> 0.2.0"
29
+ end
metadata ADDED
@@ -0,0 +1,121 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tty-config
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Piotr Murach
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-04-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.16'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.16'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: toml
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 0.2.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 0.2.0
69
+ description: Define, read and write any Ruby app configurations with a penchant for
70
+ terminal clients.
71
+ email:
72
+ - ''
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - ".gitignore"
78
+ - ".rspec"
79
+ - ".travis.yml"
80
+ - CHANGELOG.md
81
+ - CODE_OF_CONDUCT.md
82
+ - Gemfile
83
+ - LICENSE.txt
84
+ - README.md
85
+ - Rakefile
86
+ - appveyor.yml
87
+ - bin/console
88
+ - bin/setup
89
+ - lib/tty-config.rb
90
+ - lib/tty/config.rb
91
+ - lib/tty/config/version.rb
92
+ - tasks/console.rake
93
+ - tasks/coverage.rake
94
+ - tasks/spec.rake
95
+ - tty-config.gemspec
96
+ homepage: https://piotrmurach.github.io/tty
97
+ licenses:
98
+ - MIT
99
+ metadata: {}
100
+ post_install_message:
101
+ rdoc_options: []
102
+ require_paths:
103
+ - lib
104
+ required_ruby_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: 2.0.0
109
+ required_rubygems_version: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
114
+ requirements: []
115
+ rubyforge_project:
116
+ rubygems_version: 2.5.1
117
+ signing_key:
118
+ specification_version: 4
119
+ summary: Define, read and write any Ruby app configurations with a penchant for terminal
120
+ clients.
121
+ test_files: []