opencensus 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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