├── .gitignore ├── .rspec ├── .travis.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin └── setup ├── ioughta.gemspec ├── lib ├── ioughta.rb └── ioughta │ └── version.rb └── spec ├── ioughta_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 2.2.6 5 | - 2.3.3 6 | - ruby-head 7 | - jruby-9.1.7.0 8 | - jruby-head 9 | before_install: gem install bundler 10 | matrix: 11 | allow_failures: 12 | - rvm: ruby-head 13 | - rvm: jruby-head 14 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Mike Pastore 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Io(ugh)ta 2 | 3 | [![Build Status](https://travis-ci.org/mwpastore/ioughta.svg?branch=master)](https://travis-ci.org/mwpastore/ioughta) 4 | [![Gem Version](https://badge.fury.io/rb/ioughta.svg)](https://badge.fury.io/rb/ioughta) 5 | 6 | Helpers for defining sequences of constants in Ruby using a Go-like syntax. 7 | 8 | Go has quite a nice facility for defining constants derived from a sequential 9 | value using a [simple and elegant syntax][1], so I thought I'd steal it for 10 | Ruby. Rubyists tend to group constants together in hashes rather than littering 11 | their programs with countless constants, so there's a mechanism for that, too. 12 | 13 | Although there isn't as strong of a need for sequences of constants in Ruby as 14 | there is in other languages such as Go, they are still sometimes required when 15 | working with external systems such as databases and web APIs for which Ruby 16 | symbols don't map cleanly. For example, a database column might store users' 17 | privilege levels as 0, 1, or 2, and it would be useful to define constants that 18 | map to those values. Ruby doesn't have a native expression for this construct 19 | (other than simply defining them one at a time). 20 | 21 | Here's a simple example, written in Go: 22 | 23 | ```go 24 | type Allergen int 25 | 26 | const ( 27 | IgEggs Allergen = 1 << iota // 1 << 0 which is 00000001 28 | IgChocolate // 1 << 1 which is 00000010 29 | IgNuts // 1 << 2 which is 00000100 30 | IgStrawberries // 1 << 3 which is 00001000 31 | IgShellfish // 1 << 4 which is 00010000 32 | ) 33 | ``` 34 | 35 | And here it is in Ruby, using ioughta: 36 | 37 | ```ruby 38 | Object.ioughta_const( 39 | :IG_EGGS, ->(ioughta) { 1 << ioughta }, 40 | :IG_CHOCOLATE, 41 | :IG_NUTS, 42 | :IG_STRAWBERRIES, 43 | :IG_SHELLFISH 44 | ) 45 | 46 | IG_STRAWBERRIES # => 8 47 | ``` 48 | 49 | Or, perhaps a bit more Rubyishly: 50 | 51 | ```ruby 52 | IG = Object.iota_hash(%i[ 53 | eggs 54 | chocolate 55 | nuts 56 | strawberries 57 | shellfish 58 | ]) { |i| 1 << i }.freeze 59 | 60 | IG[:shellfish] # => 16 61 | ``` 62 | 63 | ## Installation 64 | 65 | Add this line to your application's Gemfile: 66 | 67 | ```ruby 68 | gem 'ioughta' 69 | ``` 70 | 71 | And then execute: 72 | 73 | ```sh 74 | $ bundle 75 | ``` 76 | 77 | Or install it yourself as: 78 | 79 | ```sh 80 | $ gem install ioughta 81 | ``` 82 | 83 | ## Usage 84 | 85 | Ioughta works just like `const` and `iota` do in Go, with only a few minor 86 | differences. You must `include` the module in your program, class, or module in 87 | order to start using it. The iterator starts at zero (0) and increments for 88 | each constant (or hash key) being defined. A function (any Ruby callable) takes 89 | the current iteration as input and returns the value to be assigned. The 90 | default function simply returns the iterator, so you can easily create 91 | sequences of constants with consecutive integer values: 92 | 93 | ```ruby 94 | require 'ioughta' 95 | include Ioughta 96 | 97 | Object.ioughta_const(:FOO, :BAR, :QUX) 98 | 99 | FOO # => 0 100 | BAR # => 1 101 | QUX # => 2 102 | ``` 103 | 104 | To skip value(s) in the sequence, use the `:_` symbol: 105 | 106 | ```ruby 107 | Object.ioughta_const(:_, :FOO, :BAR, :_, :QUX) 108 | 109 | FOO # => 1 110 | BAR # => 2 111 | QUX # => 4 112 | ``` 113 | 114 | As soon as Ioughta sees a lambda (or any Ruby callable), it will start using it 115 | to generate future values from the iterator. You can redefine the lambda as 116 | many times as you like: 117 | 118 | ```ruby 119 | Object.ioughta_const( 120 | :A, # will use the default lambda (0 => 0) 121 | :B, ->(i) { i * 2 }, # will multiply by two (1 => 2) 122 | :C, # will also multiply by two (2 => 4) 123 | :D, ->(j) { j ** 3 }, # will cube (3 => 27) 124 | :E, # will also cube (4 => 64) 125 | :F, # cube all the things (5 => 125) 126 | :G, ->{ 0.5 } # will use a simple value (6 => 0.5) 127 | :H, proc(&:itself) # restore the default behavior (7 => 7) 128 | ) 129 | ``` 130 | 131 | You can also pass the lambda as the first argument: 132 | 133 | ```ruby 134 | Object.ioughta_const ->(i) { 1 << (10 * i) }, %i[_ KiB MiB GiB TiB PiB EiB ZiB YiB] 135 | ``` 136 | 137 | Or even pass a block, instead of a lambda (it's the Ruby way!): 138 | 139 | ```ruby 140 | UNITS = Object.ioughta_hash(%i[_ KB MB GB TB PB EB ZB YB]) { |i| 10 ** (i * 3) }.freeze 141 | ``` 142 | 143 | If the first argument is a lambda *and* a block is given, the block will be 144 | silently ignored. 145 | 146 | ## Notes 147 | 148 | The only major feature missing from the Go implementation is the ability to 149 | perform parallel assignment in the constant list. We're defining a list of 150 | terms, not a list of expressions, so it's not possible to do in Ruby without 151 | resourcing to nasty `eval` tricks. **Don't forget to separate your terms with 152 | commas and freeze your hash constants!** 153 | 154 | You've probably noticed that in order to use Ioughta in the top-level 155 | namespace, we need to explicitly specify the `Object` receiver (just like we 156 | need to do for `#const_set`). I didn't want to get too crazy with the 157 | monkey-patching and/or method delegation. No such limitation exists when 158 | including Ioughta in a module or class, thanks to the available context. Also, 159 | if the `ioughta_const` and `ioughta_hash` method names are too ugly for you (I 160 | don't blame you), they're aliased as `iota_const` and `iota_hash`, 161 | respectively. 162 | 163 | Here is a very contrived and arbitrary example: 164 | 165 | ```ruby 166 | require 'ioughta' 167 | 168 | module MyFileUtils 169 | include Ioughta 170 | 171 | iota_const ->(b) { 1 << b }, %i[EXECUTE WRITE READ] 172 | iota_const ->(b) { 1 << b }, %i[TACKY SETGID SETUID] 173 | 174 | OFFSET = iota_hash(->(d) { d * 3 }, %i[other group user special]).freeze 175 | MASK = iota_hash(OFFSET.keys) { |_, key| 7 << OFFSET[key] }.freeze 176 | 177 | def self.mask_and_shift(mode, field) 178 | (mode & MASK[field]) >> OFFSET[field] 179 | end 180 | end 181 | 182 | MyFileUtils.mask_and_shift(0644, :user) & MyFileUtils::EXECUTE # => 0 183 | MyFileUtils.mask_and_shift(01777, :special) & MyFileUtils::TACKY # => 1 184 | ``` 185 | 186 | One note on the above: the lambda (or block) can take the "key" at the current 187 | iteration as an optional second argument. 188 | 189 | ## Development 190 | 191 | After checking out the repo, run `bin/setup` to install dependencies. Then, run 192 | `rake spec` to run the tests. 193 | 194 | To install this gem onto your local machine, run `bundle exec rake install`. To 195 | release a new version, update the version number in `version.rb`, and then run 196 | `bundle exec rake release`, which will create a git tag for the version, push 197 | git commits and tags, and push the `.gem` file to 198 | [rubygems.org](https://rubygems.org). 199 | 200 | ## Trivium 201 | 202 | Pronounced /aɪ ˈɔtə/, as in the English phrase "Why, I oughta...!" 203 | 204 | ## Contributing 205 | 206 | Bug reports and pull requests are welcome on GitHub at 207 | https://github.com/mwpastore/ioughta. 208 | 209 | ## License 210 | 211 | The gem is available as open source under the terms of the [MIT 212 | License](http://opensource.org/licenses/MIT). 213 | 214 | [1]: https://splice.com/blog/iota-elegant-constants-golang/ 215 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 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 9 | -------------------------------------------------------------------------------- /ioughta.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # coding: utf-8 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'ioughta/version' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = 'ioughta' 9 | spec.version = Ioughta::VERSION 10 | spec.authors = ['Mike Pastore'] 11 | spec.email = ['mike@oobak.org'] 12 | 13 | spec.summary = 'Helpers for defining sequences of constants in Ruby using a Go-like syntax' 14 | spec.homepage = 'http://github.com/mwpastore/ioughta' 15 | spec.license = 'MIT' 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 18 | f.match(%r{^(test|spec|features)/}) 19 | end 20 | spec.bindir = 'exe' 21 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 22 | spec.require_paths = ['lib'] 23 | 24 | spec.required_ruby_version = '>= 2.2.0' 25 | 26 | spec.add_development_dependency 'bundler', '~> 1.13' 27 | spec.add_development_dependency 'rake', '~> 10.0' 28 | spec.add_development_dependency 'rspec', '~> 3.0' 29 | end 30 | -------------------------------------------------------------------------------- /lib/ioughta.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'ioughta/version' 3 | 4 | module Ioughta 5 | def self.included(base) 6 | class << base 7 | def ioughta_const(*data, &block) 8 | each_resolved_ipair(data, block) do |nom, val| 9 | const_set(nom, val) 10 | end 11 | end 12 | 13 | alias_method :iota_const, :ioughta_const 14 | 15 | def ioughta_hash(*data, &block) 16 | each_resolved_ipair(data, block).to_h 17 | end 18 | 19 | alias_method :iota_hash, :ioughta_hash 20 | 21 | private 22 | 23 | DEFAULT_LAMBDA = proc(&:itself) 24 | SKIP_SYMBOL = :_ 25 | 26 | def each_ipair_with_index(data, block = nil) 27 | data = data.to_a.flatten(1) 28 | lam = (data.shift if data[0].respond_to?(:call)) || block || DEFAULT_LAMBDA 29 | 30 | data.each_with_index do |nom, i, j = i.succ| 31 | yield nom, data[j].respond_to?(:call) ? lam = data.slice!(j) : lam, i 32 | end 33 | end 34 | 35 | def each_resolved_ipair(*args) 36 | return enum_for(__method__, *args) unless block_given? 37 | 38 | each_ipair_with_index(*args) do |nom, lam, iota| 39 | yield nom, lam.call(*[iota, nom].take(lam.arity.abs)) unless nom == SKIP_SYMBOL 40 | end 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/ioughta/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | module Ioughta 3 | VERSION = '0.3.0'.freeze 4 | end 5 | -------------------------------------------------------------------------------- /spec/ioughta_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ioughta do 4 | it 'has a version number' do 5 | expect(Ioughta::VERSION).not_to be_nil 6 | end 7 | 8 | describe 'simple module constants' do 9 | before do 10 | module Foo 11 | include Ioughta 12 | 13 | ioughta_const :A, :B, :C 14 | BAR = ioughta_hash(:D, :E, :F).freeze 15 | 16 | ioughta_const :QUX, ->(a, b) { b } 17 | end 18 | end 19 | 20 | after do 21 | Object.send(:remove_const, :Foo) 22 | end 23 | 24 | it 'defines scalar constants' do 25 | expect(Foo::A).to eq(0) 26 | expect(Foo::B).to eq(1) 27 | expect(Foo::C).to eq(2) 28 | end 29 | 30 | it 'defines hash attributes' do 31 | expect(Foo::BAR).to eq(D: 0, E: 1, F: 2) 32 | end 33 | 34 | it 'aliases the methods' do 35 | expect(Foo.method(:iota_const)).to eq(Foo.method(:ioughta_const)) 36 | expect(Foo.method(:iota_hash)).to eq(Foo.method(:ioughta_hash)) 37 | end 38 | 39 | it 'allows the lambda to take an optional second argument' do 40 | expect(Foo::QUX).to eq(:QUX) 41 | end 42 | end 43 | 44 | describe 'complex module constants' do 45 | before do 46 | module Foo 47 | include Ioughta 48 | 49 | ioughta_const :_, :A, ->(i) { i ** 2 }, :B, :C 50 | BAR = ioughta_hash(:_, :D, ->(i) { i ** 3 }, :E, :F).freeze 51 | end 52 | end 53 | 54 | after do 55 | Object.send(:remove_const, :Foo) 56 | end 57 | 58 | it 'defines scalar constants' do 59 | expect(Foo::A).to eq(1 ** 2) 60 | expect(Foo::B).to eq(2 ** 2) 61 | expect(Foo::C).to eq(3 ** 2) 62 | end 63 | 64 | it 'defines hash attributes' do 65 | expect(Foo::BAR).to eq(D: 1 ** 3, E: 2 ** 3, F: 3 ** 3) 66 | end 67 | end 68 | 69 | describe 'multiple skips and lambdas' do 70 | before do 71 | module Foo 72 | include Ioughta 73 | 74 | ioughta_const \ 75 | :A, :_, 76 | :B, ->(i) { i ** 2 }, :C, :_, 77 | :D, ->(j) { j ** 3 }, :E, :F, :_, 78 | :G, ->{ 0.1 }, :H, :_, 79 | :I, proc(&:itself) 80 | end 81 | end 82 | 83 | after do 84 | Object.send(:remove_const, :Foo) 85 | end 86 | 87 | it 'defines scalar constants' do 88 | expect(Foo::A).to eq(0) 89 | expect(Foo::B).to eq(2 ** 2) 90 | expect(Foo::C).to eq(3 ** 2) 91 | expect(Foo::D).to eq(5 ** 3) 92 | expect(Foo::E).to eq(6 ** 3) 93 | expect(Foo::F).to eq(7 ** 3) 94 | expect(Foo::G).to eq(0.1) 95 | expect(Foo::H).to eq(0.1) 96 | expect(Foo::I).to eq(12) 97 | end 98 | end 99 | 100 | describe 'alternative calling styles' do 101 | before do 102 | module Foo 103 | include Ioughta 104 | 105 | ioughta_const ->(i) { 1 << (10 * i) }, %i[_ KB MB GB] 106 | 107 | BYTES = ioughta_hash(%i[_ KB MB GB]) { |iota| 1 << (10 * iota) }.freeze 108 | end 109 | end 110 | 111 | after do 112 | Object.send(:remove_const, :Foo) 113 | end 114 | 115 | it 'accepts a lambda as the first argument' do 116 | expect(Foo::KB).to eq(2 ** 10) 117 | expect(Foo::MB).to eq(2 ** 20) 118 | expect(Foo::GB).to eq(2 ** 30) 119 | end 120 | 121 | it 'accepts a block instead of a lambda' do 122 | expect(Foo::BYTES[:KB]).to eq(2 ** 10) 123 | expect(Foo::BYTES[:MB]).to eq(2 ** 20) 124 | expect(Foo::BYTES[:GB]).to eq(2 ** 30) 125 | end 126 | end 127 | 128 | describe 'esoteric keys' do 129 | before do 130 | module Foo 131 | include Ioughta 132 | 133 | FOO = ioughta_hash([ 134 | [:a, :b], 135 | {:c=>1, :d=>2}, 136 | nil 137 | ]).freeze 138 | end 139 | end 140 | 141 | after do 142 | Object.send(:remove_const, :Foo) 143 | end 144 | 145 | it "accepts weird key classes" do 146 | expect(Foo::FOO.keys[0]).to be_a(Array) 147 | expect(Foo::FOO.keys[1]).to be_a(Hash) 148 | expect(Foo::FOO.keys[2]).to be_a(NilClass) 149 | end 150 | end 151 | end 152 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../lib', __dir__) 2 | require 'ioughta' 3 | --------------------------------------------------------------------------------