├── .gitignore ├── LICENSE ├── README.md ├── fig_leaf.gemspec ├── lib └── fig_leaf.rb └── spec ├── classes_for_tests.rb └── fig_leaf_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.gem -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2012 Avdi Grimm 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | fig_leaf 2 | ======== 3 | 4 | Private inheritance for Ruby classes. 5 | 6 | FigLeaf enables us to selectively make public methods inherited from other classes and modules private. The objects can still call these methods internally, but external classes are prevented from doing so. 7 | 8 | 9 | ##Installation 10 | ``` sh 11 | gem install fig_leaf 12 | ``` 13 | or add to Gemfile 14 | ``` ruby 15 | gem 'fig_leaf' 16 | ``` 17 | 18 | ##Usage 19 | ``` ruby 20 | class Post < ActiveRecord::Base 21 | include FigLeaf 22 | 23 | hide ActiveRecord::Base, ancestors: true, except: [ 24 | Object, :init_with, :new_record?, :errors, :valid?, :save ] 25 | 26 | hide_singletons ActiveRecord::Calculations, 27 | ActiveRecord::FinderMethods, ActiveRecord::Relation 28 | 29 | # ... 30 | ``` 31 | 32 | In this code, we hide the entire `ActiveRecord::Base` interface, with just a few carefully chosen exceptions like `#valid?` and `#save`. We also hide a bunch of the more common class-level methods that ActiveRecord adds, like `.find`, `.all`, and `#count` by calling `#hide_singleton` with the modules which define those methods. 33 | 34 | Now, if we jump into the console and try to call common ActiveRecord methods on it, we are denied access: 35 | 36 | ``` 37 | ruby-1.9.2-p0 > Post.find(1) 38 | NoMethodError: private method `find' called for # 39 | 40 | ruby-1.9.2-p0 > Post.new.destroy 41 | NoMethodError: Attempt to call private method 42 | ``` 43 | 44 | FigLeaf is not intended as a hammer to keep your coworkers or your library clients in line. It's not as if that would work, anyway; the strictures that it adds are easy enough to circumvent. 45 | 46 | FigLeaf's intended role is more along the lines of the "rumble strips" along highways which give you a jolt when you veer off into the shoulder. It provides a sharp reminder when you've unthinkingly introduced a new bit of coupling to an interface you are trying to keep isolated from the rest of the codebase. Then, you can consciously make the decision whether to make that method public, or find a different way of going about what you were doing. -------------------------------------------------------------------------------- /fig_leaf.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'fig_leaf' 3 | s.version = '0.0.3' 4 | s.date = Time.now.strftime('%Y-%m-%d') 5 | s.summary = "Private inheritance for Ruby classes." 6 | s.description = "FigLeaf enables us to selectively make public methods inherited from other classes and modules private. The objects can still call these methods internally, but external classes are prevented from doing so." 7 | s.authors = ["Avdi Grimm"] 8 | s.email = ["sam@codeodor.com"] 9 | s.files = ["lib/fig_leaf.rb"] 10 | s.homepage = 'https://github.com/objects-on-rails/fig-leaf' 11 | end -------------------------------------------------------------------------------- /lib/fig_leaf.rb: -------------------------------------------------------------------------------- 1 | # Tools for making inherited interfaces private to a class. 2 | module FigLeaf 3 | module Macros 4 | private 5 | # Given a list of classes, modules, strings, and symbols, compile 6 | # a combined list of methods. Classes and modules will be queried 7 | # for their instance methods; strings and symbols will be treated 8 | # as method names. 9 | # 10 | # Once the list is compiled, make all of the methods private. 11 | # 12 | # Takes an optional options hash, which can include the following options: 13 | # 14 | # - :ancestors is a boolean determining whether to consider 15 | # ancestors classes and modules. 16 | # 17 | # - :except is a list of classes, modules, and method names which 18 | # will be excluded from treatment. 19 | def hide(*stuff) 20 | hide_methods(self, [Object], *stuff) 21 | end 22 | 23 | # Like #hide, only hides methods at the class/module level. 24 | def hide_singletons(*stuff) 25 | hide_methods(singleton_class, [Class], *stuff) 26 | end 27 | 28 | # The shared bits of #hide and #hide_singletons 29 | def hide_methods(mod, except_defaults, *stuff) 30 | options = stuff.last.is_a?(Hash) ? stuff.pop : {} 31 | include_ancestors = options.fetch(:ancestors){false} 32 | except = Array(options.fetch(:except){except_defaults}) 33 | protect = Array(options[:protect]) 34 | except_methods = collect_methods(true, *except) 35 | protect_methods = collect_methods(true, *protect) 36 | methods_to_hide = collect_methods(include_ancestors, *stuff) 37 | (methods_to_hide - except_methods).each do |method_name| 38 | mod.module_eval do 39 | next unless method_defined?(method_name) 40 | if protect_methods.include?(method_name) 41 | protected method_name 42 | else 43 | private method_name 44 | end 45 | end 46 | end 47 | end 48 | 49 | # Given a list of classes, modules, strings, and symbols, compile 50 | # a combined list of methods. Classes and modules will be queried 51 | # for their instance methods; strings and symbols will be treated 52 | # as methods names. +include_ancestors+ determines whether to 53 | # include methods defined by class/module ancestors. 54 | def collect_methods(include_ancestors, *methods_or_modules) 55 | methods_or_modules.inject([]) {|methods, method_or_module| 56 | case method_or_module 57 | when Symbol, String 58 | methods << method_or_module.to_sym 59 | when Module # also includes classes 60 | methods.concat(method_or_module.instance_methods(include_ancestors)) 61 | when Array 62 | methods.concat(method_or_module) 63 | else 64 | raise ArgumentError, "Bad argument: #{method_or_module.inspect}" 65 | end 66 | } 67 | end 68 | end 69 | 70 | def self.clothe(other) 71 | other.extend(Macros) 72 | end 73 | 74 | def self.included(other) 75 | clothe(other) 76 | other.singleton_class.extend(Macros) 77 | end 78 | 79 | def self.extended(object) 80 | clothe(object.singleton_class) 81 | end 82 | end -------------------------------------------------------------------------------- /spec/classes_for_tests.rb: -------------------------------------------------------------------------------- 1 | class Grandparent 2 | def grandparent_public_instance_method; 42; end 3 | def self.grandparent_public_class_method; 42; end 4 | end 5 | 6 | class Parent < Grandparent 7 | def parent_public_instance_method; 42; end 8 | def self.parent_public_class_method; 42; end 9 | end 10 | 11 | class Child < Parent 12 | def child_public_instance_method; 42; end 13 | def second_child_public_instance_method; 42; end 14 | def self.child_public_class_method; 42; end 15 | end -------------------------------------------------------------------------------- /spec/fig_leaf_spec.rb: -------------------------------------------------------------------------------- 1 | gem 'minitest' # demand gem version 2 | require 'minitest/autorun' 3 | require_relative '../lib/fig_leaf' 4 | 5 | def wipe_classes 6 | if defined? Grandparent 7 | Object.send(:remove_const, :Grandparent) 8 | Object.send(:remove_const, :Parent) 9 | Object.send(:remove_const, :Child) 10 | end 11 | load File.join(File.dirname(__FILE__), 'classes_for_tests.rb') 12 | end 13 | 14 | 15 | describe 'FigLeaf' do 16 | before :each do 17 | wipe_classes 18 | end 19 | 20 | it 'hides a single class method' do 21 | Child.child_public_class_method.must_equal 42 22 | class Child 23 | include FigLeaf 24 | hide_singletons :child_public_class_method 25 | end 26 | proc { Child.child_public_class_method }.must_raise NoMethodError 27 | end 28 | 29 | it 'hides instance methods' do 30 | Child.new.child_public_instance_method.must_equal 42 31 | class Child 32 | include FigLeaf 33 | hide :child_public_instance_method 34 | end 35 | proc { Child.new.child_public_instance_method }.must_raise NoMethodError 36 | end 37 | 38 | it 'deeply hides methods from ancestor objects' do 39 | Child.new.grandparent_public_instance_method.must_equal 42 40 | Child.new.parent_public_instance_method.must_equal 42 41 | class Child 42 | include FigLeaf 43 | hide Parent, ancestors: true 44 | end 45 | proc { Child.new.grandparent_public_instance_method }.must_raise NoMethodError 46 | proc { Child.new.parent_public_instance_method }.must_raise NoMethodError 47 | end 48 | 49 | it 'does not hide ancestors if not asked to' do 50 | Child.new.grandparent_public_instance_method.must_equal 42 51 | class Child 52 | include FigLeaf 53 | hide Parent 54 | end 55 | Child.new.grandparent_public_instance_method.must_equal 42 56 | end 57 | 58 | it 'allows you to specify single instance method to keep visible' do 59 | Child.new.child_public_instance_method.must_equal 42 60 | Child.new.second_child_public_instance_method.must_equal 42 61 | class Child 62 | include FigLeaf 63 | hide self, except: :second_child_public_instance_method 64 | end 65 | proc { Child.new.child_public_instance_method }.must_raise NoMethodError 66 | Child.new.second_child_public_instance_method.must_equal 42 67 | end 68 | 69 | it 'allows you to specify entire class instance method exceptions to keep visible' do 70 | Child.new.grandparent_public_instance_method.must_equal 42 71 | Child.new.parent_public_instance_method.must_equal 42 72 | class Child 73 | include FigLeaf 74 | hide Parent, ancestors: true, except: [Grandparent] 75 | end 76 | Child.new.grandparent_public_instance_method.must_equal 42 77 | proc { Child.new.parent_public_instance_method }.must_raise NoMethodError 78 | end 79 | 80 | it 'allows you to specify more than one exception to keep visible' do 81 | Child.new.child_public_instance_method.must_equal 42 82 | Child.new.second_child_public_instance_method.must_equal 42 83 | Child.new.grandparent_public_instance_method.must_equal 42 84 | class Child 85 | include FigLeaf 86 | hide self, except: [:second_child_public_instance_method, :grandparent_public_instance_method] 87 | end 88 | proc { Child.new.child_public_instance_method }.must_raise NoMethodError 89 | Child.new.second_child_public_instance_method.must_equal 42 90 | Child.new.grandparent_public_instance_method.must_equal 42 91 | end 92 | 93 | it 'does not pollute your interface by making its own methods public' do 94 | proc { Child.hide(Parent) }.must_raise NoMethodError 95 | class Child 96 | include FigLeaf 97 | hide self 98 | end 99 | proc { Child.hide(Parent) }.must_raise NoMethodError 100 | end 101 | end 102 | --------------------------------------------------------------------------------