frequency-dsl 0.0.7 → 0.2.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
+ SHA256:
3
+ metadata.gz: 885a326a495e7ef1629d6ba950b3796f8590b9193c5aa662a53423bbb111b751
4
+ data.tar.gz: 1e179bb453f710beca1f720f1c5c626cb117655a64a822004fc18d249111a1c5
5
+ SHA512:
6
+ metadata.gz: 895c4e9dbb8bd2393109b5b8aeb67dde7ef8d2c1142ba30bb765fbc417098d443aebafd00bf09d491238176a21c12a7d5aec7331973006904a5d7a21035fe4a4
7
+ data.tar.gz: 98cb4ea17df2116de1dbbcac49e4144e6bc14c7d3da2a84c99c6c19e9de9665b994f0a3542392387083b1d6692ea29eb0f919d703f92145aa750aaa7aecb59a3
data/CHANGELOG.md ADDED
@@ -0,0 +1,41 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
+ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [0.2.0] - 2026
8
+
9
+ ### Changed (BREAKING)
10
+
11
+ - `with_probability` is now a keyword argument: use
12
+ `sometimes(with_probability: 0.1) { ... }` instead of the old
13
+ `sometimes(:with_probability => 0.1) { ... }`. The hash form no longer works.
14
+ - Invalid probabilities now raise `Frequency::InvalidProbabilityError`
15
+ (subclass of `Frequency::Error < StandardError`) instead of a bare
16
+ `RuntimeError`.
17
+ - Minimum Ruby version is now 3.1.
18
+
19
+ ### Added
20
+
21
+ - `Frequency.random=` to inject a custom `Random` instance for reproducible runs.
22
+ - `Frequency.with_seed(seed) { ... }` to scope a seeded RNG to a block.
23
+ - Module-function call style: `Frequency.sometimes { ... }` works without
24
+ `include Frequency`.
25
+ - GitHub Actions CI matrix across Ruby 3.1–3.4.
26
+ - GitHub Actions release workflow using RubyGems trusted publishing (OIDC).
27
+ - RuboCop config and lint job.
28
+
29
+ ### Fixed
30
+
31
+ - Percent-string regex was using `.` (any char) instead of `\.`, so malformed
32
+ inputs like `"50x5%"` silently parsed. Now properly anchored and validated.
33
+ - Probability validation now produces a clear error type and message.
34
+
35
+ ### Removed
36
+
37
+ - Jeweler dependency and the generated `rdoc/` directory.
38
+
39
+ ## [0.1.x] - 2010
40
+
41
+ Initial release. See git history.
data/README.md ADDED
@@ -0,0 +1,192 @@
1
+ # Frequency
2
+
3
+ [![CI](https://github.com/peczenyj/Frequency/actions/workflows/ci.yml/badge.svg)](https://github.com/peczenyj/Frequency/actions/workflows/ci.yml)
4
+ [![Gem Version](https://badge.fury.io/rb/frequency-dsl.svg)](https://rubygems.org/gems/frequency-dsl)
5
+
6
+ A small Ruby DSL for executing blocks with controlled probability:
7
+ `always`, `normally`, `sometimes`, `rarely`, `never`.
8
+
9
+ Useful for sampled logging, chaos testing, demo data generation, or anywhere
10
+ you want a side effect to fire "occasionally" without writing `rand < 0.25`
11
+ by hand every time.
12
+
13
+ ```ruby
14
+ require "frequency"
15
+
16
+ Frequency.always { metrics.flush }
17
+ Frequency.normally { cache.write(key, value) }
18
+ Frequency.sometimes { logger.debug(payload) }
19
+ Frequency.rarely { dump_full_state }
20
+ Frequency.never { not_in_production }
21
+ ```
22
+
23
+ ## Installation
24
+
25
+ ```ruby
26
+ # Gemfile
27
+ gem "frequency-dsl"
28
+ ```
29
+
30
+ ```sh
31
+ bundle install
32
+ # or, standalone:
33
+ gem install frequency-dsl
34
+ ```
35
+
36
+ Requires Ruby 3.1 or newer.
37
+
38
+ ## Usage
39
+
40
+ Two call styles are supported — pick whichever fits your code.
41
+
42
+ ### Module-function style
43
+
44
+ Recommended for libraries and anywhere you want explicit namespacing:
45
+
46
+ ```ruby
47
+ require "frequency"
48
+
49
+ Frequency.sometimes { puts "maybe" }
50
+ Frequency.rarely(with_probability: 0.01) { puts "1% of the time" }
51
+ Frequency.normally(with_probability: "24%") { puts "string percent" }
52
+ ```
53
+
54
+ ### Mixin style
55
+
56
+ Convenient at the script level or inside a host class:
57
+
58
+ ```ruby
59
+ require "frequency"
60
+
61
+ class EventLogger
62
+ include Frequency
63
+
64
+ def record(event)
65
+ sometimes { write_to_disk(event) }
66
+ rarely(with_probability: "0.1%") { ship_for_audit(event) }
67
+ end
68
+ end
69
+ ```
70
+
71
+ ### The `with_probability:` keyword
72
+
73
+ `sometimes`, `normally`, and `rarely` accept an optional `with_probability:`
74
+ keyword argument. It overrides the method's default rate. Accepted values:
75
+
76
+ | Value | Meaning |
77
+ | --------------- | -------------------- |
78
+ | `0.42` | Float in `[0, 1]` |
79
+ | `"42%"` | Percent string |
80
+ | `"42.5%"` | Fractional percent |
81
+ | `"0.42"` | Numeric string |
82
+
83
+ The keyword is ignored by `always` (always runs) and `never` (never runs).
84
+
85
+ ### Defaults
86
+
87
+ ```ruby
88
+ Frequency.normally { ... } # 75%
89
+ Frequency.sometimes { ... } # 50%
90
+ Frequency.rarely { ... } # 25%
91
+ Frequency.maybe { ... } # alias of .sometimes
92
+ ```
93
+
94
+ ### Reproducible runs
95
+
96
+ Inject your own RNG:
97
+
98
+ ```ruby
99
+ Frequency.random = Random.new(42)
100
+ ```
101
+
102
+ Or scope a seed to a single block — the previous RNG is restored afterward,
103
+ even if the block raises:
104
+
105
+ ```ruby
106
+ Frequency.with_seed(42) do
107
+ 10.times { Frequency.sometimes { puts "deterministic" } }
108
+ end
109
+ ```
110
+
111
+ This is especially useful in tests, where you want `sometimes` to behave
112
+ predictably without mocking `Kernel.rand`.
113
+
114
+ ### Errors
115
+
116
+ Invalid probabilities raise `Frequency::InvalidProbabilityError`
117
+ (a subclass of `Frequency::Error < StandardError`):
118
+
119
+ ```ruby
120
+ Frequency.sometimes(with_probability: "101%") { ... }
121
+ # => Frequency::InvalidProbabilityError: probability must be in [0, 1], got 1.01
122
+
123
+ Frequency.sometimes(with_probability: "50x5%") { ... }
124
+ # => Frequency::InvalidProbabilityError: could not parse probability: "50x5%"
125
+
126
+ Frequency.sometimes(with_probability: :half) { ... }
127
+ # => Frequency::InvalidProbabilityError: unsupported probability type: Symbol
128
+ ```
129
+
130
+ ## Development
131
+
132
+ After cloning:
133
+
134
+ ```sh
135
+ bin/setup # bundle install
136
+ bundle exec rake # specs + lint (Standard)
137
+ bundle exec rspec # specs only
138
+ bin/console # IRB with Frequency loaded
139
+ ```
140
+
141
+ Continuous integration runs against Ruby 3.1, 3.2, 3.3, and 3.4 on every
142
+ push and pull request — see [`.github/workflows/ci.yml`](.github/workflows/ci.yml).
143
+
144
+ ## Releasing
145
+
146
+ Releases go to RubyGems via [trusted publishing][tp] — no long-lived API
147
+ tokens stored as repository secrets. A push of a `v*` tag triggers
148
+ [`.github/workflows/release.yml`](.github/workflows/release.yml), which
149
+ runs specs, builds the gem, and authenticates to RubyGems via short-lived
150
+ OIDC credentials.
151
+
152
+ **One-time setup** (gem owner only):
153
+
154
+ 1. On <https://rubygems.org/gems/frequency-dsl>, open **Trusted publishers**
155
+ and create one with:
156
+ - Owner: `peczenyj`
157
+ - Repository: `Frequency`
158
+ - Workflow filename: `release.yml`
159
+ - Environment: `release`
160
+ 2. In this repo, go to **Settings → Environments** and create an environment
161
+ named `release`. Optional but recommended: add a required reviewer so each
162
+ release needs manual approval.
163
+
164
+ **Cutting a release:**
165
+
166
+ ```sh
167
+ # Bump Frequency::VERSION in lib/frequency.rb,
168
+ # update CHANGELOG.md (move [Unreleased] entries under the new version),
169
+ # then:
170
+ git commit -am "Release v0.2.0"
171
+ git tag v0.2.0
172
+ git push origin master --tags
173
+ ```
174
+
175
+ [tp]: https://guides.rubygems.org/trusted-publishing/
176
+
177
+ ## Contributing
178
+
179
+ Bug reports and pull requests are welcome on
180
+ [GitHub](https://github.com/peczenyj/Frequency).
181
+
182
+ 1. Fork and create a branch
183
+ 2. Run `bin/setup`
184
+ 3. Make your change, add specs
185
+ 4. `bundle exec rake` — both specs and lint must pass
186
+ 5. Open a PR
187
+
188
+ ## License
189
+
190
+ Apache License 2.0 — see [LICENSE](LICENSE).
191
+
192
+ Copyright © 2010–2026 Tiago Peczenyj.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.7
1
+ 0.2.0
data/lib/frequency.rb CHANGED
@@ -1,65 +1,117 @@
1
- # Module Frequency
2
- # A small dsl written in ruby to work with frequency events (never, sometimes, always..)
1
+ # frozen_string_literal: true
2
+
3
+ # A small DSL to execute blocks with controlled probability.
3
4
  #
5
+ # require "frequency"
6
+ #
7
+ # # Module-function style:
8
+ # Frequency.sometimes { puts "maybe" }
9
+ # Frequency.rarely(with_probability: 0.01) { log_sample(event) }
10
+ # Frequency.normally(with_probability: "24%") { do_thing }
11
+ #
12
+ # # Or mixin style:
13
+ # include Frequency
14
+ # sometimes { puts "maybe" }
4
15
  module Frequency
5
- NORMALLY = 0.75
6
- SOMETIMES = 0.50
7
- RARELY = 0.25
8
-
9
- # always do something
10
- # example:
11
- # always { puts "ok"}
12
- def always(cond={})
16
+ VERSION = "0.2.0"
17
+
18
+ # Base error class. Catch this to handle any Frequency-raised error.
19
+ Error = Class.new(StandardError)
20
+
21
+ # Raised when the probability value is not a number/string in [0, 1].
22
+ InvalidProbabilityError = Class.new(Error)
23
+
24
+ DEFAULTS = {
25
+ normally: 0.75,
26
+ sometimes: 0.50,
27
+ rarely: 0.25
28
+ }.freeze
29
+
30
+ PERCENT_PATTERN = /\A-?\d+(?:\.\d+)?%\z/
31
+
32
+ # Always run the block. Returns the block's value, or nil if no block.
33
+ def always
13
34
  yield if block_given?
14
35
  end
15
-
16
- # normally (75%) do something,
17
- # see sometimes method
18
- # example:
19
- # normally { puts "ok"}
20
- def normally(cond={})
21
- execute_with_probability(cond,NORMALLY ) { yield } if block_given?
22
- end
23
-
24
- # sometimes (50%) do something
25
- # example:
26
- # sometimes do
27
- # # some code
28
- # end
29
- # or adjusting the probability
30
- # sometimes :with_probability => 0.12 do
31
- # # some code
32
- # end
33
- # you can use "12%" instead 0.12
34
- # sometimes(:with_probability => '13.6%') { ... }
35
- def sometimes(cond={})
36
- execute_with_probability(cond,SOMETIMES) { yield } if block_given?
36
+
37
+ # Never run the block. Returns nil. Ignores any arguments/block.
38
+ def never(*)
39
+ nil
37
40
  end
38
-
39
- # rarely (25%) do something,
40
- # see sometimes method
41
- # example:
42
- # rarely { puts "ok"}
43
- def rarely(cond={})
44
- execute_with_probability(cond,RARELY ) { yield } if block_given?
41
+
42
+ # Run the block ~75% of the time by default.
43
+ def normally(with_probability: DEFAULTS[:normally], &block)
44
+ Frequency.__run(with_probability, &block)
45
45
  end
46
-
47
- # never do something
48
- # example:
49
- # never { puts "ok"}
50
- def never(cond={})
51
- nil
46
+
47
+ # Run the block ~50% of the time by default.
48
+ def sometimes(with_probability: DEFAULTS[:sometimes], &block)
49
+ Frequency.__run(with_probability, &block)
52
50
  end
53
-
54
- private
55
- def execute_with_probability(conditions,default)
56
- rate = conditions[:with_probability] || default
57
- rate = Float(rate) rescue correct_if_string(rate)
58
- raise "probability must be [0..100]%" unless 0 <= rate && rate <= 1
59
- yield if Kernel.rand() < rate
51
+
52
+ # Alias of #sometimes.
53
+ alias_method :maybe, :sometimes
54
+
55
+ # Run the block ~25% of the time by default.
56
+ def rarely(with_probability: DEFAULTS[:rarely], &block)
57
+ Frequency.__run(with_probability, &block)
60
58
  end
61
-
62
- def correct_if_string(rate)
63
- Float(rate.gsub(/%$/,""))/100 if rate =~ /^-?\d+(.\d+)?%$/
59
+
60
+ # Make every public instance method also a module-level method, so both
61
+ # `include Frequency; sometimes { ... }` and `Frequency.sometimes { ... }`
62
+ # work. Unlike `module_function`, `extend self` keeps the methods public
63
+ # when the module is included as a mixin.
64
+ extend self
65
+
66
+ class << self
67
+ # Inject a custom Random instance for reproducible runs.
68
+ # Frequency.random = Random.new(42)
69
+ attr_writer :random
70
+
71
+ def random
72
+ @random ||= Random.new
73
+ end
74
+
75
+ # Run a block with a temporarily-seeded RNG, then restore the previous one.
76
+ # Frequency.with_seed(42) { sometimes { ... } }
77
+ def with_seed(seed)
78
+ previous = @random
79
+ @random = Random.new(seed)
80
+ yield
81
+ ensure
82
+ @random = previous
83
+ end
84
+
85
+ # @api private
86
+ def __run(probability, &block)
87
+ return nil unless block
88
+
89
+ rate = __coerce(probability)
90
+ unless (0.0..1.0).cover?(rate)
91
+ raise InvalidProbabilityError, "probability must be in [0, 1], got #{rate}"
92
+ end
93
+
94
+ block.call if random.rand < rate
95
+ end
96
+
97
+ private
98
+
99
+ def __coerce(value)
100
+ case value
101
+ when Numeric then value.to_f
102
+ when String then __parse_string(value)
103
+ else raise InvalidProbabilityError, "unsupported probability type: #{value.class}"
104
+ end
105
+ end
106
+
107
+ def __parse_string(str)
108
+ if PERCENT_PATTERN.match?(str)
109
+ Float(str.chomp("%")) / 100.0
110
+ else
111
+ Float(str)
112
+ end
113
+ rescue ArgumentError, TypeError
114
+ raise InvalidProbabilityError, "could not parse probability: #{str.inspect}"
115
+ end
64
116
  end
65
117
  end
metadata CHANGED
@@ -1,98 +1,54 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: frequency-dsl
3
- version: !ruby/object:Gem::Version
4
- hash: 17
5
- prerelease: false
6
- segments:
7
- - 0
8
- - 0
9
- - 7
10
- version: 0.0.7
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
11
5
  platform: ruby
12
- authors:
6
+ authors:
13
7
  - Tiago Peczenyj
14
- autorequire:
8
+ autorequire:
15
9
  bindir: bin
16
10
  cert_chain: []
17
-
18
- date: 2010-05-19 00:00:00 -03:00
19
- default_executable:
20
- dependencies:
21
- - !ruby/object:Gem::Dependency
22
- name: rspec
23
- prerelease: false
24
- requirement: &id001 !ruby/object:Gem::Requirement
25
- none: false
26
- requirements:
27
- - - ">="
28
- - !ruby/object:Gem::Version
29
- hash: 3
30
- segments:
31
- - 0
32
- version: "0"
33
- type: :development
34
- version_requirements: *id001
35
- description: A small dsl written in ruby to work with frequency events (never, sometimes, always..)
36
- email: tiago.peczenyj@gmail.com
11
+ date: 2026-05-14 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Frequency is a small DSL written in Ruby to work with frequency events
14
+ (never, sometimes, always, ...).
15
+ email:
16
+ - tiago.peczenyj@gmail.com
37
17
  executables: []
38
-
39
18
  extensions: []
40
-
41
- extra_rdoc_files:
19
+ extra_rdoc_files: []
20
+ files:
21
+ - CHANGELOG.md
42
22
  - LICENSE
43
- - README.rdoc
44
- files:
45
- - LICENSE
46
- - README.rdoc
47
- - Rakefile
23
+ - README.md
48
24
  - VERSION
49
- - frequency-dsl.gemspec
50
25
  - lib/frequency.rb
51
- - rdoc/classes/Frequency.html
52
- - rdoc/created.rid
53
- - rdoc/files/README_rdoc.html
54
- - rdoc/files/lib/frequency_rb.html
55
- - rdoc/fr_class_index.html
56
- - rdoc/fr_file_index.html
57
- - rdoc/fr_method_index.html
58
- - rdoc/index.html
59
- - rdoc/rdoc-style.css
60
- - spec/frequency_spec.rb
61
- - spec/spec_helper.rb
62
- has_rdoc: true
63
- homepage: http://github.com/peczenyj/Frequency
64
- licenses: []
65
-
66
- post_install_message:
67
- rdoc_options:
68
- - --charset=UTF-8
69
- require_paths:
26
+ homepage: https://github.com/peczenyj/Frequency
27
+ licenses:
28
+ - Apache-2.0
29
+ metadata:
30
+ homepage_uri: https://github.com/peczenyj/Frequency
31
+ source_code_uri: https://github.com/peczenyj/Frequency
32
+ bug_tracker_uri: https://github.com/peczenyj/Frequency/issues
33
+ changelog_uri: https://github.com/peczenyj/Frequency/blob/master/CHANGELOG.md
34
+ rubygems_mfa_required: 'true'
35
+ post_install_message:
36
+ rdoc_options: []
37
+ require_paths:
70
38
  - lib
71
- required_ruby_version: !ruby/object:Gem::Requirement
72
- none: false
73
- requirements:
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ requirements:
74
41
  - - ">="
75
- - !ruby/object:Gem::Version
76
- hash: 3
77
- segments:
78
- - 0
79
- version: "0"
80
- required_rubygems_version: !ruby/object:Gem::Requirement
81
- none: false
82
- requirements:
42
+ - !ruby/object:Gem::Version
43
+ version: 3.1.0
44
+ required_rubygems_version: !ruby/object:Gem::Requirement
45
+ requirements:
83
46
  - - ">="
84
- - !ruby/object:Gem::Version
85
- hash: 3
86
- segments:
87
- - 0
88
- version: "0"
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
89
49
  requirements: []
90
-
91
- rubyforge_project:
92
- rubygems_version: 1.3.7
93
- signing_key:
94
- specification_version: 3
95
- summary: A small dsl written in ruby to work with frequency events
96
- test_files:
97
- - spec/frequency_spec.rb
98
- - spec/spec_helper.rb
50
+ rubygems_version: 3.5.22
51
+ signing_key:
52
+ specification_version: 4
53
+ summary: A small DSL for probabilistic event execution
54
+ test_files: []
data/README.rdoc DELETED
@@ -1,41 +0,0 @@
1
- = Frequency
2
-
3
- Frequency is a small dsl written in ruby to work with frequency events (never, sometimes, always..)
4
-
5
- == Examples
6
-
7
- Frequency is easy...
8
-
9
- require 'rubygems'
10
- require 'frequency'
11
-
12
- include Frequency
13
-
14
- sometimes do
15
- puts "sometimes you can see this times, sometimes not"
16
- end
17
-
18
- never do
19
- puts "this line never will be print"
20
- end
21
-
22
- rarely :with_probability => 0.01 do
23
- puts "ok, its very rare..."
24
- end
25
-
26
- normally :with_probability => '24%' do
27
- puts "you can use strings instead float numbers"
28
- end
29
-
30
- always do
31
- puts "bye"
32
- end
33
-
34
- the option with_probability does not work with never and always.
35
-
36
- == Install
37
- [sudo] gem install frequency-dsl
38
-
39
- == Copyright
40
-
41
- Copyright (c) 2010 Tiago Peczenyj. See LICENSE for details.
data/Rakefile DELETED
@@ -1,51 +0,0 @@
1
- require 'rubygems'
2
- require 'rake'
3
-
4
- begin
5
- require 'jeweler'
6
- Jeweler::Tasks.new do |gem|
7
- gem.name = "frequency-dsl"
8
- gem.summary = %Q{A small dsl written in ruby to work with frequency events}
9
- gem.description = %Q{A small dsl written in ruby to work with frequency events (never, sometimes, always..)}
10
- gem.email = "tiago.peczenyj@gmail.com"
11
- gem.homepage = "http://github.com/peczenyj/Frequency"
12
- gem.authors = ["Tiago Peczenyj"]
13
- gem.add_development_dependency "rspec"
14
- # gem is a Gem::Specification...
15
- # See http://www.rubygems.org/read/chapter/20 for additional settings
16
- end
17
- Jeweler::GemcutterTasks.new
18
- rescue LoadError
19
- puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
20
- end
21
-
22
- require 'spec/rake/spectask'
23
- Spec::Rake::SpecTask.new(:spec) do |spec|
24
- spec.libs << 'lib' << 'spec'
25
- spec.spec_files = FileList['spec/**/*_spec.rb']
26
- end
27
-
28
- Spec::Rake::SpecTask.new(:rcov) do |spec|
29
- spec.libs << 'lib' << 'spec'
30
- spec.pattern = 'spec/**/*_spec.rb'
31
- spec.rcov = true
32
- end
33
-
34
- task :spec => :check_dependencies
35
-
36
- task :default => :spec
37
-
38
- require 'rake/rdoctask'
39
- Rake::RDocTask.new do |rdoc|
40
- if File.exist?('VERSION')
41
- version = File.read('VERSION')
42
- else
43
- version = ""
44
- end
45
-
46
- rdoc.rdoc_dir = 'rdoc'
47
- rdoc.title = "frequency #{version}"
48
- rdoc.rdoc_files.include('README*')
49
- rdoc.rdoc_files.include('lib/**/*.rb')
50
- end
51
-