opencensus 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.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.rubocop.yml +48 -0
  4. data/.travis.yml +16 -0
  5. data/AUTHORS +1 -0
  6. data/CODE_OF_CONDUCT.md +43 -0
  7. data/CONTRIBUTING.md +34 -0
  8. data/Gemfile +4 -0
  9. data/LICENSE +201 -0
  10. data/README.md +180 -0
  11. data/Rakefile +20 -0
  12. data/bin/console +14 -0
  13. data/bin/setup +8 -0
  14. data/docs/.gitignore +3 -0
  15. data/docs/404.html +24 -0
  16. data/docs/Gemfile +31 -0
  17. data/docs/_config.yml +39 -0
  18. data/docs/_layouts/default.html +65 -0
  19. data/docs/index.md +151 -0
  20. data/lib/opencensus.rb +21 -0
  21. data/lib/opencensus/common.rb +24 -0
  22. data/lib/opencensus/common/config.rb +521 -0
  23. data/lib/opencensus/config.rb +54 -0
  24. data/lib/opencensus/context.rb +72 -0
  25. data/lib/opencensus/stats.rb +26 -0
  26. data/lib/opencensus/tags.rb +25 -0
  27. data/lib/opencensus/trace.rb +181 -0
  28. data/lib/opencensus/trace/annotation.rb +60 -0
  29. data/lib/opencensus/trace/config.rb +119 -0
  30. data/lib/opencensus/trace/exporters.rb +26 -0
  31. data/lib/opencensus/trace/exporters/logger.rb +149 -0
  32. data/lib/opencensus/trace/formatters.rb +29 -0
  33. data/lib/opencensus/trace/formatters/binary.rb +66 -0
  34. data/lib/opencensus/trace/formatters/cloud_trace.rb +102 -0
  35. data/lib/opencensus/trace/formatters/trace_context.rb +124 -0
  36. data/lib/opencensus/trace/integrations.rb +24 -0
  37. data/lib/opencensus/trace/integrations/faraday_middleware.rb +176 -0
  38. data/lib/opencensus/trace/integrations/rack_middleware.rb +127 -0
  39. data/lib/opencensus/trace/integrations/rails.rb +121 -0
  40. data/lib/opencensus/trace/link.rb +90 -0
  41. data/lib/opencensus/trace/message_event.rb +80 -0
  42. data/lib/opencensus/trace/samplers.rb +50 -0
  43. data/lib/opencensus/trace/samplers/always_sample.rb +34 -0
  44. data/lib/opencensus/trace/samplers/max_qps.rb +55 -0
  45. data/lib/opencensus/trace/samplers/never_sample.rb +34 -0
  46. data/lib/opencensus/trace/samplers/probability.rb +69 -0
  47. data/lib/opencensus/trace/span.rb +196 -0
  48. data/lib/opencensus/trace/span_builder.rb +560 -0
  49. data/lib/opencensus/trace/span_context.rb +308 -0
  50. data/lib/opencensus/trace/status.rb +49 -0
  51. data/lib/opencensus/trace/time_event.rb +38 -0
  52. data/lib/opencensus/trace/trace_context_data.rb +22 -0
  53. data/lib/opencensus/trace/truncatable_string.rb +61 -0
  54. data/lib/opencensus/version.rb +18 -0
  55. data/opencensus.gemspec +32 -0
  56. metadata +210 -0
data/Rakefile ADDED
@@ -0,0 +1,20 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+ require "yard"
4
+
5
+ require "rubocop/rake_task"
6
+ RuboCop::RakeTask.new
7
+
8
+ Rake::TestTask.new :test do |t|
9
+ t.libs << "test"
10
+ t.libs << "lib"
11
+ t.test_files = FileList["test/**/*_test.rb"]
12
+ end
13
+
14
+ YARD::Rake::YardocTask.new do |t|
15
+ t.files = ['lib/**/*.rb'] # optional
16
+ t.options = ['--output-dir', 'docs/api'] # optional
17
+ t.stats_options = ['--list-undoc'] # optional
18
+ end
19
+
20
+ task :default => [:test, :rubocop, :yard]
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "opencensus"
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/docs/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ _site
2
+ .sass-cache
3
+ .jekyll-metadata
data/docs/404.html ADDED
@@ -0,0 +1,24 @@
1
+ ---
2
+ layout: default
3
+ ---
4
+
5
+ <style type="text/css" media="screen">
6
+ .container {
7
+ margin: 10px auto;
8
+ max-width: 600px;
9
+ text-align: center;
10
+ }
11
+ h1 {
12
+ margin: 30px 0;
13
+ font-size: 4em;
14
+ line-height: 1;
15
+ letter-spacing: -1px;
16
+ }
17
+ </style>
18
+
19
+ <div class="container">
20
+ <h1>404</h1>
21
+
22
+ <p><strong>Page not found :(</strong></p>
23
+ <p>The requested page could not be found.</p>
24
+ </div>
data/docs/Gemfile ADDED
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+ source "https://rubygems.org"
3
+
4
+ # Hello! This is where you manage which Jekyll version is used to run.
5
+ # When you want to use a different version, change it below, save the
6
+ # file and run `bundle install`. Run Jekyll with `bundle exec`, like so:
7
+ #
8
+ # bundle exec jekyll serve
9
+ #
10
+ # This will help ensure the proper Jekyll version is running.
11
+ # Happy Jekylling!
12
+ gem "jekyll", "~> 3.6.2"
13
+
14
+ # This is the default theme for new Jekyll sites. You may change this to anything you like.
15
+ # gem "minima", "~> 2.0"
16
+ gem "jekyll-theme-minimal", "~> 0.1"
17
+ # gem "minimal"
18
+
19
+ # If you want to use GitHub Pages, remove the "gem "jekyll"" above and
20
+ # uncomment the line below. To upgrade, run `bundle update github-pages`.
21
+ # gem "github-pages", group: :jekyll_plugins
22
+
23
+ # If you have any plugins, put them here!
24
+ group :jekyll_plugins do
25
+ gem "jekyll-feed", "~> 0.6"
26
+ end
27
+
28
+ # Windows does not include zoneinfo files, so bundle the tzinfo-data gem
29
+ gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
30
+
31
+ gem "github-pages", group: :jekyll_plugins
data/docs/_config.yml ADDED
@@ -0,0 +1,39 @@
1
+ # Welcome to Jekyll!
2
+ #
3
+ # This config file is meant for settings that affect your whole blog, values
4
+ # which you are expected to set up once and rarely edit after that. If you find
5
+ # yourself editing this file very often, consider using Jekyll's data files
6
+ # feature for the data you need to update frequently.
7
+ #
8
+ # For technical reasons, this file is *NOT* reloaded automatically when you use
9
+ # 'bundle exec jekyll serve'. If you change this file, please restart the server process.
10
+
11
+ # Site settings
12
+ # These are used to personalize your new site. If you look in the HTML files,
13
+ # you will see them accessed via {{ site.title }}, {{ site.email }}, and so on.
14
+ # You can create any custom variable you would like, and they will be accessible
15
+ # in the templates via {{ site.myvariable }}.
16
+ title: OpenCensus for Ruby
17
+ email: your-email@example.com
18
+ description: A stats collection and distributed tracing framework
19
+ baseurl: "/opencensus-ruby" # the subpath of your site, e.g. /blog
20
+ url: "http://opencensus.io" # the base hostname & protocol for your site, e.g. http://example.com
21
+ github_username: census-instrumentation
22
+
23
+ # Build settings
24
+ markdown: kramdown
25
+ theme: jekyll-theme-minimal
26
+ plugins:
27
+ - jekyll-feed
28
+
29
+ # Exclude from processing.
30
+ # The following items will not be processed, by default. Create a custom list
31
+ # to override the default setting.
32
+ # exclude:
33
+ # - Gemfile
34
+ # - Gemfile.lock
35
+ # - node_modules
36
+ # - vendor/bundle/
37
+ # - vendor/cache/
38
+ # - vendor/gems/
39
+ # - vendor/ruby/
@@ -0,0 +1,65 @@
1
+ <!doctype html>
2
+ <html lang="{{ site.lang | default: "en-US" }}">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta http-equiv="X-UA-Compatible" content="chrome=1">
6
+
7
+ {% seo %}
8
+
9
+ <link rel="stylesheet" href="{{ '/assets/css/style.css?v=' | append: site.github.build_revision | relative_url }}">
10
+ <meta name="viewport" content="width=device-width">
11
+ <!--[if lt IE 9]>
12
+ <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
13
+ <![endif]-->
14
+ </head>
15
+ <body>
16
+ <div class="wrapper">
17
+ <header>
18
+ <h1>{{ site.title | default: site.github.repository_name }}</h1>
19
+ <p>{{ site.description | default: site.github.project_tagline }}</p>
20
+
21
+ {% if site.github.is_project_page %}
22
+ <p class="view"><a href="{{ site.github.repository_url }}">View the Project on GitHub <small>{{ github_name }}</small></a></p>
23
+ {% endif %}
24
+ <p class="view"><a href="{{ '/api' | relative_url }}">View the API documentation</a>
25
+
26
+ {% if site.github.is_user_page %}
27
+ <p class="view"><a href="{{ site.github.owner_url }}">View My GitHub Profile</a></p>
28
+ {% endif %}
29
+
30
+ {% if site.show_downloads %}
31
+ <ul>
32
+ <li><a href="{{ site.github.zip_url }}">Download <strong>ZIP File</strong></a></li>
33
+ <li><a href="{{ site.github.tar_url }}">Download <strong>TAR Ball</strong></a></li>
34
+ <li><a href="{{ site.github.repository_url }}">View On <strong>GitHub</strong></a></li>
35
+ </ul>
36
+ {% endif %}
37
+ </header>
38
+ <section>
39
+
40
+ {{ content }}
41
+
42
+ </section>
43
+ <footer>
44
+ {% if site.github.is_project_page %}
45
+ <p>This project is maintained by <a href="{{ site.github.owner_url }}">{{ site.github.owner_name }}</a></p>
46
+ {% endif %}
47
+ <p><small>Hosted on GitHub Pages &mdash; Theme by <a href="https://github.com/orderedlist">orderedlist</a></small></p>
48
+ </footer>
49
+ </div>
50
+ <script src="{{ '/assets/js/scale.fix.js' | relative_url }}"></script>
51
+
52
+
53
+ {% if site.google_analytics %}
54
+ <script>
55
+ (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
56
+ (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
57
+ m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
58
+ })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
59
+
60
+ ga('create', '{{ site.google_analytics }}', 'auto');
61
+ ga('send', 'pageview');
62
+ </script>
63
+ {% endif %}
64
+ </body>
65
+ </html>
data/docs/index.md ADDED
@@ -0,0 +1,151 @@
1
+ ---
2
+ layout: default
3
+ ---
4
+
5
+ # OpenCensus for Ruby
6
+
7
+ OpenCensus provides a framework to measure a server's resource usage and
8
+ collect performance stats. The `opencensus` Rubygem contains the core
9
+ OpenCensus APIs and basic integrations with Rails, Faraday, and GRPC.
10
+
11
+ The library is in alpha stage, and the API is subject to change.
12
+
13
+ ## Quick Start
14
+
15
+ ### Installation
16
+
17
+ Install the gem directly:
18
+
19
+ ```sh
20
+ $ gem install opencensus
21
+ ```
22
+
23
+ Or install through Bundler:
24
+
25
+ 1. Add the `opencensus` gem to your Gemfile:
26
+
27
+ ```ruby
28
+ gem "opencensus"
29
+ ```
30
+
31
+ 2. Use Bundler to install the gem:
32
+
33
+ ```sh
34
+ $ bundle install
35
+ ```
36
+
37
+ ### Getting started with Ruby on Rails
38
+
39
+ The OpenCensus library provides a Railtie that integrates with Ruby On Rails,
40
+ automatically tracing incoming requests in the application. It also
41
+ automatically traces key processes in your application such as database queries
42
+ and view rendering.
43
+
44
+ To enable Rails integration, require this file during application startup:
45
+
46
+ ```ruby
47
+ # In config/application.rb
48
+ require "opencensus/trace/integrations/rails"
49
+ ```
50
+
51
+ ### Getting started with other Rack-based frameworks
52
+
53
+ Other Rack-based frameworks, such as Sinatra, can use the Rack Middleware
54
+ integration, which automatically traces incoming requests. To enable the
55
+ integration for a non-Rails Rack framework, add the middleware to your
56
+ middleware stack.
57
+
58
+ ```ruby
59
+ # In config.ru or similar Rack configuration file
60
+ require "opencensus/trace/integrations/rack_middleware"
61
+ use OpenCensus::Trace::Integrations::RackMiddleware
62
+ ```
63
+
64
+ ## Instrumentation features
65
+
66
+ ### Tracing outgoing HTTP requests
67
+
68
+ If your app uses the [Faraday](https://github.com/lostisland/faraday) library
69
+ to make outgoing HTTP requests, consider installing the Faraday Middleware
70
+ integration. This integration creates a span for each outgoing Faraday request,
71
+ tracking the latency of that request, and propagates distributed trace headers
72
+ into the request so you can potentially connect your request trace with that of
73
+ the remote service. Here is an example:
74
+
75
+ ```ruby
76
+ conn = Faraday.new(url: "http://www.example.com") do |c|
77
+ c.use OpenCensus::Trace::Integrations::FaradayMiddleware
78
+ c.adapter Faraday.default_adapter
79
+ end
80
+ conn.get "/"
81
+ ```
82
+
83
+ See the documentation for the
84
+ [FaradayMiddleware](http://opencensus.io/opencensus-ruby/api/OpenCensus/Trace/Integrations/FaradayMiddleware.html)
85
+ class for more info.
86
+
87
+ ### Adding Custom Trace Spans
88
+
89
+ In addition to the spans added by the Rails integration (e.g. for database
90
+ queries) and by Faraday integration for outgoing HTTP requests, you can add
91
+ additional custom spans to the request trace:
92
+
93
+ ```ruby
94
+ OpenCensus::Trace.in_span "my_task" do |span|
95
+ # Do stuff...
96
+
97
+ OpenCensus::Trace.in_span "my_subtask" do |subspan|
98
+ # Do other stuff
99
+ end
100
+ end
101
+ ```
102
+
103
+ See the documentation for the
104
+ [OpenCensus::Trace](http://opencensus.io/opencensus-ruby/api/OpenCensus/Trace.html)
105
+ module for more info.
106
+
107
+ ### Exporting traces
108
+
109
+ By default, OpenCensus will log request trace data as JSON. To export traces to
110
+ your favorite analytics backend, install an export plugin. There are plugins
111
+ currently being developed for Stackdriver, Zipkin, and other services.
112
+
113
+ ### Configuring the library
114
+
115
+ OpenCensus allows configuration of a number of aspects via the configuration
116
+ class. The following example illustrates how that looks:
117
+
118
+ ```ruby
119
+ OpenCensus.configure do |c|
120
+ c.trace.default_sampler = OpenCensus::Trace::Samplers::AlwaysSample.new
121
+ c.trace.default_max_attributes = 16
122
+ end
123
+ ```
124
+
125
+ If you are using Rails, you can equivalently use the Rails config:
126
+
127
+ ```ruby
128
+ config.opencensus.trace.default_sampler =
129
+ OpenCensus::Trace::Samplers::AlwaysSample.new
130
+ config.opencensus.trace.default_max_attributes = 16
131
+ ```
132
+
133
+ You can configure a variety of core OpenCensuys options, including:
134
+
135
+ * Sampling, which controls how often a request is traced.
136
+ * Exporting, which controls how trace information is reported.
137
+ * Formatting, which controls how distributed request trace headers are
138
+ constructed
139
+ * Size maximums, which control when trace data is truncated.
140
+
141
+ Additionally, integrations and other plugins might have their own
142
+ configurations.
143
+
144
+ For more information, consult the documentation for
145
+ [OpenCensus.configure](http://opencensus.io/opencensus-ruby/api/OpenCensus.html#configure-class_method)
146
+ and
147
+ [OpenCensus::Trace.configure](http://opencensus.io/opencensus-ruby/api/OpenCensus/Trace.html#configure-class_method).
148
+
149
+ ## Supported Ruby Versions
150
+
151
+ This library is supported on Ruby 2.0+.
data/lib/opencensus.rb ADDED
@@ -0,0 +1,21 @@
1
+ # Copyright 2017 OpenCensus Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require "opencensus/common"
16
+ require "opencensus/config"
17
+ require "opencensus/context"
18
+ require "opencensus/stats"
19
+ require "opencensus/tags"
20
+ require "opencensus/trace"
21
+ require "opencensus/version"
@@ -0,0 +1,24 @@
1
+ # Copyright 2017 OpenCensus Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require "opencensus/common/config"
16
+
17
+ module OpenCensus
18
+ ##
19
+ # The Common module contains common infrastructure that can be shared between
20
+ # Trace and Stats (not yet implemented)
21
+ #
22
+ module Common
23
+ end
24
+ end
@@ -0,0 +1,521 @@
1
+ # Copyright 2017 OpenCensus Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module OpenCensus
16
+ module Common
17
+ ##
18
+ # OpenCensus configuration class.
19
+ #
20
+ # Configuration mechanism for OpenCensus libraries. A Config object contains
21
+ # a list of predefined keys, some of which are values and others of which
22
+ # are subconfigurations, i.e. categories. Option values are generally
23
+ # validated to ensure they are the correct type.
24
+ #
25
+ # You generally access fields and subconfigs by calling accessor methods.
26
+ # Only explicitly defined fields have these accessor methods defined.
27
+ # Methods meant for "administration" such as adding options, are always
28
+ # named with a trailing "!" or "?" so they don't pollute the method
29
+ # namespace.
30
+ #
31
+ # Example:
32
+ #
33
+ # config = OpenCensus::Common::Config.new do |c|
34
+ # c.add_option! :opt1, 10
35
+ # c.add_option! :opt2, :one, enum: [:one, :two, :three]
36
+ # c.add_option! :opt3, "hi", match: [String, Symbol]
37
+ # c.add_option! :opt4, "hi", match: /^[a-z]+$/, allow_nil: true
38
+ # c.add_config! :sub do |c2|
39
+ # c2.add_option! :opt5, false
40
+ # end
41
+ # end
42
+ #
43
+ # config.opt1 #=> 10
44
+ # config.opt1 = 20 #=> 20
45
+ # config.opt1 #=> 20
46
+ # config.opt1 = "hi" #=> exception (only Integer allowed)
47
+ # config.opt1 = nil #=> exception (nil not allowed)
48
+ #
49
+ # config.opt2 #=> :one
50
+ # config.opt2 = :two #=> :two
51
+ # config.opt2 #=> :two
52
+ # config.opt2 = :four #=> exception (not in allowed enum)
53
+ # config.opt2 = nil #=> exception (nil not allowed)
54
+ #
55
+ # config.opt3 #=> "hi"
56
+ # config.opt3 = "hiho" #=> "hiho"
57
+ # config.opt3 #=> "hiho"
58
+ # config.opt3 = "HI" #=> exception (regexp check failed)
59
+ # config.opt3 = nil #=> exception (nil not allowed)
60
+ #
61
+ # config.opt4 #=> "yo"
62
+ # config.opt4 = :yo #=> :yo (Strings and Symbols allowed)
63
+ # config.opt4 #=> :yo
64
+ # config.opt4 = 3.14 #=> exception (not in allowed types)
65
+ # config.opt4 = nil #=> nil (nil explicitly allowed)
66
+ #
67
+ # config.sub #=> <OpenCensus::Common::Config>
68
+ #
69
+ # config.sub.opt5 #=> false
70
+ # config.sub.opt5 = true #=> true (true and false allowed)
71
+ # config.sub.opt5 #=> true
72
+ # config.sub.opt5 = nil #=> exception (nil not allowed)
73
+ #
74
+ # config.opt9 #=> exception (unknown key)
75
+ # config.sub.opt9 #=> exception (unknown key)
76
+ #
77
+ class Config
78
+ ##
79
+ # Constructs a Configuration object. If a block is given, yields `self`
80
+ # to the block, which makes it convenient to initialize the structure by
81
+ # making calls to {Config#add_option!}, {Config#add_config!}, and
82
+ # {Config#add_alias!}.
83
+ #
84
+ def initialize
85
+ @fields = {}
86
+ yield self if block_given?
87
+ end
88
+
89
+ ##
90
+ # Add an option field to this configuration.
91
+ #
92
+ # You must provide a key, which becomes the field name in this config.
93
+ # Field names may comprise only letters, numerals, and underscores, and
94
+ # must begin with a letter. This will create accessor methods for the
95
+ # new configuration key.
96
+ #
97
+ # You may pass an initial value (which defaults to nil if not provided).
98
+ #
99
+ # You may also specify how values are validated. Validation is defined
100
+ # as follows:
101
+ #
102
+ # * If you provide a block or a `:validator` option, it is used as the
103
+ # validator. A proposed value is passed to the proc, and it should
104
+ # return true or false to indicate whether the value is acceptable.
105
+ # * If you provide a `:match` option, it is compared to the proposed
106
+ # value using the `===` operator. You may, for example, provide a
107
+ # class, a regular expression, or a range. If you pass an array,
108
+ # the value is accepted if _any_ of the elements match.
109
+ # * If you provide an `:enum` option, it should be an `Enumerable`.
110
+ # A proposed value is accepted if it is included.
111
+ # * Otherwise if you do not provide any of the above options, then a
112
+ # default validation strategy is inferred from the initial value:
113
+ # * If the initial is `true` or `false`, then either boolean value
114
+ # is considered valid. This is the same as `enum: [true, false]`.
115
+ # * If the initial is `nil`, then any object is considered valid.
116
+ # * Otherwise, any object of the same class as the initial value is
117
+ # considered valid. This is effectively the same as
118
+ # `match: initial.class`.
119
+ # * You may also provide the `:allow_nil` option, which, if set to
120
+ # true, alters any of the above validators to allow `nil` values.
121
+ # If the initial value is `nil` but a specific validator is provided
122
+ # via `:match` or `:enum`, then `:allow_nil` defaults to true,
123
+ # otherwise it defaults to false.
124
+ #
125
+ # In many cases, you may find that the default validation behavior
126
+ # (interpreted from the initial value) is sufficient. If you want to
127
+ # accept any value, use `match: Object`.
128
+ #
129
+ # @param [String, Symbol] key The name of the option
130
+ # @param [Object] initial Initial value (defaults to nil)
131
+ # @param [Hash] opts Validation options
132
+ #
133
+ # @return [Config] self for chaining
134
+ #
135
+ def add_option! key, initial = nil, opts = {}, &block
136
+ key = validate_new_key! key
137
+ opts[:validator] = block if block
138
+ validator = resolve_validator! initial, opts
139
+ validate_value! key, validator, initial
140
+ @fields[key] = Option.new initial, initial, validator
141
+ define_getter_method! key
142
+ define_setter_method! key
143
+ self
144
+ end
145
+
146
+ ##
147
+ # Add a subconfiguration field to this configuration.
148
+ #
149
+ # You must provide a key, which becomes the method name that you use to
150
+ # navigate to the subconfig. Names may comprise only letters, numerals,
151
+ # and underscores, and must begin with a letter.
152
+ #
153
+ # If you provide a block, the subconfig object is passed to the block,
154
+ # so you can easily add fields.
155
+ #
156
+ # @param [String, Symbol] key The name of the subconfig
157
+ #
158
+ # @return [Config] self for chaining
159
+ #
160
+ def add_config! key, &block
161
+ key = validate_new_key! key
162
+ @fields[key] = Config.new(&block)
163
+ define_getter_method! key
164
+ self
165
+ end
166
+
167
+ ##
168
+ # Add a field to this configuration that is an alias of some other
169
+ # object, which may be another field or another configuration. This will
170
+ # effectively become an alternate "path" to that same object.
171
+ #
172
+ # The following cases are supported:
173
+ #
174
+ # * Alias another configuration at this key by providing a `config`
175
+ # parameter but not a `key`. The given configuration is effectively
176
+ # "attached" as a subconfiguration; both the original configuration
177
+ # path, and this new key, point to the same configuration object and
178
+ # share configuration data.
179
+ # * Alias another field of this current configuration by providing a
180
+ # `key` parameter but not a `config`. The new key simply refers to the
181
+ # same object (which may be an option or a subconfig) as the original
182
+ # key, and shares the same data.
183
+ # * Alias another field or another configuration, by providing both a
184
+ # `config` parameter and a `key` parameter.
185
+ #
186
+ # @param [String, Symbol] new_key The key to alias.
187
+ # @param [Config, nil] config The original configuration.
188
+ # @param [String, Symbol, nil] key The original field name.
189
+ #
190
+ # @return [Config] self for chaining
191
+ #
192
+ def add_alias! new_key, config: nil, key: nil
193
+ new_key = validate_new_key! new_key
194
+ if config.nil? && key.nil?
195
+ raise ArgumentError, "You must provide a config and/or key."
196
+ end
197
+ field =
198
+ if key.nil?
199
+ config
200
+ else
201
+ (config || self).raw_field! key
202
+ end
203
+ @fields[new_key] = field
204
+ define_getter_method! new_key
205
+ define_setter_method! new_key if field.is_a? Option
206
+ self
207
+ end
208
+
209
+ ##
210
+ # Restore the original default value of the given key.
211
+ # If the key refers to a subconfiguration, restore its contents,
212
+ # recursively. If the key is omitted, restore the original defaults for
213
+ # all keys, including subconfigurations recursively.
214
+ #
215
+ # @param [Symbol, nil] key The key to reset. If omitted or `nil`,
216
+ # recursively reset all fields and subconfigs.
217
+ #
218
+ def reset! key = nil
219
+ if key.nil?
220
+ # rubocop:disable Performance/HashEachMethods
221
+ @fields.keys.each { |k| reset! k }
222
+ # rubocop:enable Performance/HashEachMethods
223
+ else
224
+ key = key.to_sym
225
+ unless @fields.key? key
226
+ raise ArgumentError, "Key #{key.inspect} does not exist"
227
+ end
228
+ field = @fields[key]
229
+ if field.is_a? Config
230
+ field.reset!
231
+ else
232
+ field.value = field.default
233
+ end
234
+ end
235
+ self
236
+ end
237
+
238
+ ##
239
+ # Remove the given key from the configuration.
240
+ # If the key is omitted, deletes all keys.
241
+ #
242
+ # Note the actual object being referenced is not touched. So if a deleted
243
+ # option is an alias of some other option, the other option will remain
244
+ # and retain the setting. Similarly, if a subconfig is referenced
245
+ # elsewhere, it will remain accessible from that other location.
246
+ #
247
+ # @param [Symbol, nil] key The key to delete. If omitted or `nil`,
248
+ # delete all fields and subconfigs.
249
+ #
250
+ def delete! key = nil
251
+ if key.nil?
252
+ @fields.clear
253
+ else
254
+ key = key.to_sym
255
+ unless @fields.key? key
256
+ raise ArgumentError, "Key #{key.inspect} does not exist"
257
+ end
258
+ field = @fields.delete key
259
+ singleton_class.send :remove_method, :"#{key}"
260
+ singleton_class.send :remove_method, :"#{key}=" if field.is_a? Option
261
+ end
262
+ self
263
+ end
264
+
265
+ ##
266
+ # Assign an option with the given name to the given value.
267
+ #
268
+ # @param [Symbol, String] key The option name
269
+ # @param [Object] value The new option value
270
+ #
271
+ def []= key, value
272
+ key = key.to_sym
273
+ unless @fields.key? key
274
+ raise ArgumentError, "Key #{key.inspect} does not exist"
275
+ end
276
+ field = @fields[key]
277
+ if field.is_a? Config
278
+ raise ArgumentError, "Key #{key.inspect} is a subconfig"
279
+ end
280
+ validate_value! key, field.validator, value
281
+ field.value = value
282
+ end
283
+
284
+ ##
285
+ # Get the option or subconfig with the given name.
286
+ #
287
+ # @param [Symbol, String] key The option or subconfig name
288
+ # @return [Object] The option value or subconfig object
289
+ #
290
+ def [] key
291
+ key = key.to_sym
292
+ unless @fields.key? key
293
+ raise ArgumentError, "Key #{key.inspect} does not exist"
294
+ end
295
+ field = @fields[key]
296
+ if field.is_a? Config
297
+ field
298
+ else
299
+ field.value
300
+ end
301
+ end
302
+
303
+ ##
304
+ # Check if this Config object has an option of the given name.
305
+ #
306
+ # @param [Symbol] key The key to check for.
307
+ # @return [boolean] true if the inquired key is a valid option for this
308
+ # Config object. False otherwise.
309
+ #
310
+ def option? key
311
+ @fields[key.to_sym].is_a? Option
312
+ end
313
+
314
+ ##
315
+ # Check if this Config object has a subconfig of the given name.
316
+ #
317
+ # @param [Symbol] key The key to check for.
318
+ # @return [boolean] true if the inquired key is a valid subconfig of this
319
+ # Config object. False otherwise.
320
+ #
321
+ def subconfig? key
322
+ @fields[key.to_sym].is_a? Config
323
+ end
324
+
325
+ ##
326
+ # Check if this Config object has a key of the given name, regardless of
327
+ # whether it is an option or a subconfig.
328
+ #
329
+ # @param [Symbol] key The key to check for.
330
+ # @return [boolean] true if the key exists.
331
+ #
332
+ def key? key
333
+ @fields.key? key.to_sym
334
+ end
335
+
336
+ ##
337
+ # Return a list of valid option names.
338
+ #
339
+ # @return [Array<Symbol>] a list of option names as symbols.
340
+ #
341
+ def options!
342
+ @fields.keys.find_all { |key| @fields[key].is_a? Option }
343
+ end
344
+
345
+ ##
346
+ # Return a list of valid subconfig names.
347
+ #
348
+ # @return [Array<Symbol>] a list of subconfig names as symbols.
349
+ #
350
+ def subconfigs!
351
+ @fields.keys.find_all { |key| @fields[key].is_a? Config }
352
+ end
353
+
354
+ ##
355
+ # Return a list of valid keys, including both options and subconfigs.
356
+ #
357
+ # @return [Array<Symbol>] a list of keys as symbols.
358
+ #
359
+ def keys!
360
+ @fields.keys
361
+ end
362
+
363
+ ##
364
+ # Returns a string representation of this configuration state.
365
+ #
366
+ # @return [String]
367
+ #
368
+ def to_s!
369
+ elems = @fields.map do |k, v|
370
+ vstr =
371
+ if v.is_a? Config
372
+ v.to_s!
373
+ else
374
+ v.value.inspect
375
+ end
376
+ " #{k}=#{vstr}"
377
+ end
378
+ "<Config#{elems.join}>"
379
+ end
380
+
381
+ ##
382
+ # Returns a nested hash representation of this configuration state,
383
+ # including subconfigurations.
384
+ #
385
+ # @return [Hash]
386
+ #
387
+ def to_h!
388
+ @fields.transform_values { |v| v.is_a?(Config) ? v.to_h! : v.value }
389
+ end
390
+
391
+ ##
392
+ # Override the default to_s implementation.
393
+ #
394
+ # @private
395
+ #
396
+ def to_s
397
+ to_s!
398
+ end
399
+
400
+ ##
401
+ # Override the default inspect implementation.
402
+ #
403
+ # @private
404
+ #
405
+ def inspect
406
+ to_s!
407
+ end
408
+
409
+ ##
410
+ # Override the default to_h implementation.
411
+ #
412
+ # @private
413
+ #
414
+ def to_h
415
+ to_h!
416
+ end
417
+
418
+ protected
419
+
420
+ ##
421
+ # Get the raw value of the field hash for the given key.
422
+ #
423
+ # @private
424
+ #
425
+ def raw_field! key
426
+ key = key.to_sym
427
+ unless @fields.key? key
428
+ raise ArgumentError, "Key #{key.inspect} does not exist"
429
+ end
430
+ @fields[key]
431
+ end
432
+
433
+ private
434
+
435
+ ##
436
+ # Internal data structure to hold configuration options
437
+ #
438
+ # @private
439
+ #
440
+ Option = Struct.new :value, :default, :validator
441
+
442
+ ##
443
+ # A validator that allows all values
444
+ #
445
+ # @private
446
+ #
447
+ OPEN_VALIDATOR = ::Proc.new { true }
448
+
449
+ def validate_new_key! key
450
+ key_str = key.to_s
451
+ unless key_str =~ /^\w+$/
452
+ raise ArgumentError, "Illegal key: #{key_str.inspect}"
453
+ end
454
+ key = key.to_sym
455
+ if @fields.key? key
456
+ raise ArgumentError, "Key #{key.inspect} already exists"
457
+ end
458
+ key
459
+ end
460
+
461
+ def resolve_validator! initial, opts
462
+ allow_nil = initial.nil? || opts[:allow_nil]
463
+ if opts.key? :validator
464
+ build_proc_validator! opts[:validator], allow_nil
465
+ elsif opts.key? :match
466
+ build_match_validator! opts[:match], allow_nil
467
+ elsif opts.key? :enum
468
+ build_enum_validator! opts[:enum], allow_nil
469
+ elsif [true, false].include? initial
470
+ build_enum_validator! [true, false], allow_nil
471
+ elsif initial.nil?
472
+ OPEN_VALIDATOR
473
+ else
474
+ build_match_validator! initial.class, allow_nil
475
+ end
476
+ end
477
+
478
+ def build_match_validator! matches, allow_nil
479
+ matches = Array(matches)
480
+ matches += [nil] if allow_nil && !matches.include?(nil)
481
+ ->(val) { matches.any? { |m| m === val } }
482
+ end
483
+
484
+ def build_enum_validator! allowed, allow_nil
485
+ allowed = Array(allowed)
486
+ allowed += [nil] if allow_nil && !allowed.include?(nil)
487
+ ->(val) { allowed.include? val }
488
+ end
489
+
490
+ def build_proc_validator! proc, allow_nil
491
+ ->(val) { proc.call(val) || allow_nil && val.nil? }
492
+ end
493
+
494
+ def validate_value! key, validator, value
495
+ unless validator.call value
496
+ raise ArgumentError,
497
+ "Invalid value #{value.inspect} for key #{key.inspect}"
498
+ end
499
+ end
500
+
501
+ def define_getter_method! key
502
+ define_singleton_method key do
503
+ field = @fields[key]
504
+ if field.is_a? Config
505
+ field
506
+ else
507
+ field.value
508
+ end
509
+ end
510
+ end
511
+
512
+ def define_setter_method! key
513
+ define_singleton_method :"#{key}=" do |value|
514
+ field = @fields[key]
515
+ validate_value! key, field.validator, value
516
+ field.value = value
517
+ end
518
+ end
519
+ end
520
+ end
521
+ end