tty-config 0.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.
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: []