sinatra_resource 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 (75) hide show
  1. data/.document +5 -0
  2. data/.gitignore +7 -0
  3. data/LICENSE +20 -0
  4. data/README.mdown +28 -0
  5. data/Rakefile +34 -0
  6. data/VERSION +1 -0
  7. data/examples/datacatalog/Rakefile +23 -0
  8. data/examples/datacatalog/app.rb +15 -0
  9. data/examples/datacatalog/config/config.rb +66 -0
  10. data/examples/datacatalog/config/config.yml +11 -0
  11. data/examples/datacatalog/config.ru +6 -0
  12. data/examples/datacatalog/lib/base.rb +9 -0
  13. data/examples/datacatalog/lib/resource.rb +73 -0
  14. data/examples/datacatalog/lib/roles.rb +15 -0
  15. data/examples/datacatalog/models/categorization.rb +31 -0
  16. data/examples/datacatalog/models/category.rb +28 -0
  17. data/examples/datacatalog/models/source.rb +33 -0
  18. data/examples/datacatalog/models/user.rb +51 -0
  19. data/examples/datacatalog/resources/categories.rb +32 -0
  20. data/examples/datacatalog/resources/sources.rb +35 -0
  21. data/examples/datacatalog/resources/users.rb +26 -0
  22. data/examples/datacatalog/tasks/db.rake +29 -0
  23. data/examples/datacatalog/tasks/test.rake +16 -0
  24. data/examples/datacatalog/test/helpers/assertions/assert_include.rb +17 -0
  25. data/examples/datacatalog/test/helpers/assertions/assert_not_include.rb +17 -0
  26. data/examples/datacatalog/test/helpers/lib/model_factories.rb +49 -0
  27. data/examples/datacatalog/test/helpers/lib/model_helpers.rb +30 -0
  28. data/examples/datacatalog/test/helpers/lib/request_helpers.rb +53 -0
  29. data/examples/datacatalog/test/helpers/model_test_helper.rb +5 -0
  30. data/examples/datacatalog/test/helpers/resource_test_helper.rb +5 -0
  31. data/examples/datacatalog/test/helpers/shared/api_keys.rb +48 -0
  32. data/examples/datacatalog/test/helpers/shared/common_body_responses.rb +15 -0
  33. data/examples/datacatalog/test/helpers/shared/status_codes.rb +61 -0
  34. data/examples/datacatalog/test/helpers/test_cases/model_test_case.rb +6 -0
  35. data/examples/datacatalog/test/helpers/test_cases/resource_test_case.rb +36 -0
  36. data/examples/datacatalog/test/helpers/test_helper.rb +36 -0
  37. data/examples/datacatalog/test/models/categorization_test.rb +40 -0
  38. data/examples/datacatalog/test/models/category_test.rb +35 -0
  39. data/examples/datacatalog/test/models/source_test.rb +37 -0
  40. data/examples/datacatalog/test/models/user_test.rb +77 -0
  41. data/examples/datacatalog/test/resources/categories/categories_delete_test.rb +112 -0
  42. data/examples/datacatalog/test/resources/categories/categories_get_many_test.rb +58 -0
  43. data/examples/datacatalog/test/resources/categories/categories_get_one_test.rb +75 -0
  44. data/examples/datacatalog/test/resources/categories/categories_post_test.rb +135 -0
  45. data/examples/datacatalog/test/resources/categories/categories_put_test.rb +140 -0
  46. data/examples/datacatalog/test/resources/sources/sources_delete_test.rb +112 -0
  47. data/examples/datacatalog/test/resources/sources/sources_get_many_test.rb +58 -0
  48. data/examples/datacatalog/test/resources/sources/sources_get_one_test.rb +74 -0
  49. data/examples/datacatalog/test/resources/sources/sources_post_test.rb +184 -0
  50. data/examples/datacatalog/test/resources/sources/sources_put_test.rb +227 -0
  51. data/examples/datacatalog/test/resources/users/users_delete_test.rb +134 -0
  52. data/examples/datacatalog/test/resources/users/users_get_many_test.rb +111 -0
  53. data/examples/datacatalog/test/resources/users/users_get_one_test.rb +75 -0
  54. data/examples/datacatalog/test/resources/users/users_post_test.rb +142 -0
  55. data/examples/datacatalog/test/resources/users/users_put_test.rb +171 -0
  56. data/lib/builder/helpers.rb +319 -0
  57. data/lib/builder/mongo_helpers.rb +70 -0
  58. data/lib/builder.rb +84 -0
  59. data/lib/exceptions.rb +10 -0
  60. data/lib/resource.rb +171 -0
  61. data/lib/roles.rb +163 -0
  62. data/lib/sinatra_resource.rb +6 -0
  63. data/notes/keywords.mdown +1 -0
  64. data/notes/permissions.mdown +181 -0
  65. data/notes/questions.mdown +18 -0
  66. data/notes/see_also.mdown +3 -0
  67. data/notes/synonyms.mdown +7 -0
  68. data/notes/to_do.mdown +7 -0
  69. data/notes/uniform_interface.mdown +22 -0
  70. data/sinatra_resource.gemspec +183 -0
  71. data/spec/sinatra_resource_spec.rb +7 -0
  72. data/spec/spec_helper.rb +9 -0
  73. data/tasks/spec.rake +13 -0
  74. data/tasks/yard.rake +13 -0
  75. metadata +253 -0
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
6
+ doc/*
7
+ .yardoc
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 David James
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.mdown ADDED
@@ -0,0 +1,28 @@
1
+ ## About
2
+
3
+ Want to build Web services in the Resource Oriented Architecture style? With resource_sinatra, success is all but guaranteed, provided that you are using a [Sinatra](http://sinatrarb.com) + [MongoMapper](http://github.com/djsun/mongomapper) stack.
4
+
5
+ ## Installation
6
+
7
+ It might not be a bad idea to make sure you are running the latest RubyGems:
8
+
9
+ sudo gem update --system
10
+
11
+ You will need gemcutter if you don't have it already:
12
+
13
+ gem install gemcutter
14
+ gem tumble
15
+
16
+ I recommend a user-level install:
17
+
18
+ gem install djsun-resource_sinatra
19
+
20
+ Note: in general, beware of `sudo gem install <project_name>` -- it gives elevated privileges. Do you trust `<project name>`? Better to be safe and use a local install to `~/.gem`.
21
+
22
+ ## Usage
23
+
24
+ For a basic example of what this looks like when integrated into a real-world Sinatra app, see `/examples/datacatalog`.
25
+
26
+ ## History
27
+
28
+ This code was extracted from the [National Data Catalog](http://groups.google.com/group/datacatalog), a project of the [Sunlight Labs](http://sunlightlabs.com).
data/Rakefile ADDED
@@ -0,0 +1,34 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "sinatra_resource"
8
+ gem.summary = %Q{RESTful actions with Sinatra and MongoMapper}
9
+ gem.description = %Q{A DSL for creating RESTful actions with Sinatra and MongoMapper. It embraces the Resource Oriented Architecture as explained by Leonard Richardson and Sam Ruby.}
10
+ gem.email = "djames@sunlightfoundation.com"
11
+ gem.homepage = "http://github.com/djsun/sinatra_resource"
12
+ gem.authors = ["David James"]
13
+
14
+ gem.add_dependency 'djsun-mongo_mapper', '= 0.5.5.3'
15
+ gem.add_dependency 'mongo', '>= 0.15.1'
16
+ gem.add_dependency 'sinatra', '>= 0.9.4'
17
+
18
+ gem.add_development_dependency 'crack', '>= 0.1.4'
19
+ gem.add_development_dependency 'djsun-context', '>= 0.5.6'
20
+ gem.add_development_dependency 'jeremymcanally-pending', '>= 0.1'
21
+ gem.add_development_dependency 'rspec'
22
+ gem.add_development_dependency 'yard'
23
+
24
+ # gem is a Gem::Specification ...
25
+ # ... see http://www.rubygems.org/read/chapter/20 for additional settings
26
+ end
27
+ Jeweler::GemcutterTasks.new
28
+ rescue LoadError
29
+ puts "Jeweler (or a dependency) not available. Install with: gem install jeweler"
30
+ end
31
+
32
+ task :default => :spec
33
+
34
+ Dir.glob(File.dirname(__FILE__) + '/tasks/*.rake').each { |f| load f }
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,23 @@
1
+ require 'rake/testtask'
2
+ require 'rake/rdoctask'
3
+ require 'rcov/rcovtask'
4
+
5
+ require File.dirname(__FILE__) + '/config/config'
6
+ Dir.glob(File.dirname(__FILE__) + '/tasks/*.rake').each { |f| load f }
7
+
8
+ desc "Default: run all tests"
9
+ task :default => :test
10
+
11
+ namespace :environment do
12
+ task :application do
13
+ puts "Loading application environment..."
14
+ require File.dirname(__FILE__) + '/app'
15
+ end
16
+
17
+ task :models do
18
+ puts "Loading models..."
19
+ Config.setup_mongomapper
20
+ base = File.dirname(__FILE__)
21
+ Dir.glob(base + '/models/*.rb' ).each { |f| require f }
22
+ end
23
+ end
@@ -0,0 +1,15 @@
1
+ require 'rubygems'
2
+
3
+ gem 'sinatra', '>= 0.9.4'
4
+ require 'sinatra/base'
5
+
6
+ require File.dirname(__FILE__) + '/config/config'
7
+
8
+ Sinatra::Base.set(:config, Config.environment_config)
9
+ Config.setup
10
+
11
+ base = File.dirname(__FILE__)
12
+ Dir.glob(base + '/lib/*.rb' ).each { |f| require f }
13
+ Dir.glob(base + '/models/*.rb' ).each { |f| require f }
14
+ Dir.glob(base + '/resource_helpers/*.rb').each { |f| require f }
15
+ Dir.glob(base + '/resources/*.rb' ).each { |f| require f }
@@ -0,0 +1,66 @@
1
+ require 'rubygems'
2
+
3
+ module Config
4
+
5
+ def self.setup
6
+ setup_mongomapper
7
+ # More application setup can go here...
8
+ end
9
+
10
+ def self.setup_mongomapper
11
+ gem 'djsun-mongo_mapper', '= 0.5.5.3'
12
+ require 'mongo_mapper'
13
+ MongoMapper.connection = new_mongo_connection
14
+ MongoMapper.database = environment_config['mongo_database']
15
+ end
16
+
17
+ def self.new_mongo_connection
18
+ gem 'mongo', "= 0.15.1"
19
+ require 'mongo'
20
+ Mongo::Connection.new(environment_config["mongo_hostname"])
21
+ end
22
+
23
+ def self.drop_database
24
+ database_name = environment_config["mongo_database"]
25
+ new_mongo_connection.drop_database(database_name)
26
+ database_name
27
+ end
28
+
29
+ def self.environment_config
30
+ env_config = config[environment]
31
+ unless env_config
32
+ raise "Environment config not found for #{environment.inspect}"
33
+ end
34
+ env_config
35
+ end
36
+
37
+ def self.environment
38
+ if @environment
39
+ @environment
40
+ else
41
+ @environment = if Object.const_defined?("Sinatra")
42
+ Sinatra::Base.environment.to_s
43
+ else
44
+ ENV['RACK_ENV'] || 'development'
45
+ end
46
+ end
47
+ end
48
+
49
+ def self.environment=(env)
50
+ @environment = env
51
+ end
52
+
53
+ def self.environments
54
+ config.keys
55
+ end
56
+
57
+ def self.config
58
+ if @config
59
+ @config
60
+ else
61
+ file = File.join(File.dirname(__FILE__), "config.yml")
62
+ @config = YAML.load_file(file)
63
+ end
64
+ end
65
+
66
+ end
@@ -0,0 +1,11 @@
1
+ development:
2
+ base_uri: http://localhost:3000/
3
+ mongo_hostname: localhost
4
+ mongo_database: s_r_example_1_dev
5
+ api_key_salt: 51a920cc75eb105934df9be349be68cfdd6acf1c
6
+
7
+ test:
8
+ base_uri: http://localhost:4567/
9
+ mongo_hostname: localhost
10
+ mongo_database: s_r_example_1_test
11
+ api_key_salt: 51a920cc75eb105934df9be349be68cfdd6acf1c
@@ -0,0 +1,6 @@
1
+ ENV['RACK_ENV'] = "development"
2
+ require 'app'
3
+
4
+ map("/categories") { run DataCatalog::Categories }
5
+ map("/sources") { run DataCatalog::Sources }
6
+ map("/users") { run DataCatalog::Users }
@@ -0,0 +1,9 @@
1
+ module DataCatalog
2
+
3
+ class Base < Sinatra::Base
4
+ before do
5
+ content_type :json
6
+ end
7
+ end
8
+
9
+ end
@@ -0,0 +1,73 @@
1
+ require 'uri'
2
+
3
+ module DataCatalog
4
+
5
+ module Resource
6
+
7
+ def self.included(includee)
8
+ includee.instance_eval do
9
+ include SinatraResource::Resource
10
+ end
11
+ includee.helpers do
12
+ def before_authorization(action, role)
13
+ unless role
14
+ error 401, convert(body_for(:errors, ["invalid_api_key"]))
15
+ end
16
+ if role == :anonymous && minimum_role(action) != :anonymous
17
+ error 401, convert(body_for(:errors, ["missing_api_key"]))
18
+ end
19
+ end
20
+
21
+ def convert(object)
22
+ object == "" ? "" : object.to_json
23
+ end
24
+
25
+ def full_uri(path)
26
+ base_uri = Config.environment_config["base_uri"]
27
+ URI.join(base_uri, path).to_s
28
+ end
29
+
30
+ def lookup_role(document=nil)
31
+ api_key = lookup_api_key
32
+ return :anonymous unless api_key
33
+ user = user_for(api_key)
34
+ return nil unless user
35
+ return :owner if document && owner?(user, document)
36
+ user.role.intern
37
+ end
38
+
39
+ protected
40
+
41
+ def lookup_api_key
42
+ @api_key ||= params.delete("api_key")
43
+ end
44
+
45
+ # Is +user+ the owner of +document+?
46
+ #
47
+ # First, checks to see if +user+ and +document+ are the same. After
48
+ # that, try to follow the +document.user+ relationship, if present, to
49
+ # see if that points to +user+.
50
+ #
51
+ # @param [DataCatalog::User] user
52
+ #
53
+ # @param [MongoMapper::Document] user
54
+ #
55
+ # @return [Boolean]
56
+ def owner?(user, document)
57
+ return true if user == document
58
+ return false unless document.respond_to?(:user)
59
+ document.user == user
60
+ end
61
+
62
+ def user_for(api_key)
63
+ user = User.first(:conditions => { :_api_key => api_key })
64
+ return nil unless user
65
+ raise "API key found, but user has no role" unless user.role
66
+ user
67
+ end
68
+ end
69
+ end
70
+
71
+ end
72
+
73
+ end
@@ -0,0 +1,15 @@
1
+ require File.dirname(__FILE__) + '/../../../lib/sinatra_resource'
2
+
3
+ module DataCatalog
4
+
5
+ module Roles
6
+ include SinatraResource::Roles
7
+
8
+ role :anonymous
9
+ role :basic => :anonymous
10
+ role :owner => :basic
11
+ role :curator => :basic
12
+ role :admin => [:owner, :curator]
13
+ end
14
+
15
+ end
@@ -0,0 +1,31 @@
1
+ module DataCatalog
2
+
3
+ class Categorization
4
+
5
+ include MongoMapper::Document
6
+
7
+ # == Attributes
8
+
9
+ key :source_id, String
10
+ key :category_id, String
11
+ timestamps!
12
+
13
+ # == Indices
14
+
15
+ # == Associations
16
+
17
+ belongs_to :source
18
+ belongs_to :category
19
+
20
+ # == Validations
21
+
22
+ validate :validate_associations
23
+
24
+ def validate_associations
25
+ errors.add(:source_id, "must be valid") if source.nil?
26
+ errors.add(:category_id, "must be valid") if category.nil?
27
+ end
28
+
29
+ end
30
+
31
+ end
@@ -0,0 +1,28 @@
1
+ module DataCatalog
2
+
3
+ class Category
4
+
5
+ include MongoMapper::Document
6
+
7
+ # == Attributes
8
+
9
+ key :name, String
10
+ timestamps!
11
+
12
+ # == Indices
13
+
14
+ # == Associations
15
+
16
+ many :categorizations
17
+
18
+ def sources
19
+ categorizations.map(&:source)
20
+ end
21
+
22
+ # == Validations
23
+
24
+ validates_presence_of :name
25
+
26
+ end
27
+
28
+ end
@@ -0,0 +1,33 @@
1
+ module DataCatalog
2
+
3
+ class Source
4
+
5
+ include MongoMapper::Document
6
+
7
+ # == Attributes
8
+
9
+ key :title, String
10
+ key :url, String
11
+ key :raw, Hash
12
+ timestamps!
13
+
14
+ # == Indices
15
+
16
+ ensure_index :url
17
+
18
+ # == Associations
19
+
20
+ many :categorizations
21
+
22
+ def categories
23
+ categorizations.map(&:category)
24
+ end
25
+
26
+ # == Validations
27
+
28
+ validates_presence_of :title
29
+ validates_presence_of :url
30
+
31
+ end
32
+
33
+ end
@@ -0,0 +1,51 @@
1
+ require 'digest/sha1'
2
+
3
+ module DataCatalog
4
+
5
+ class User
6
+
7
+ include MongoMapper::Document
8
+
9
+ # == Attributes
10
+
11
+ key :name, String
12
+ key :email, String
13
+ key :role, String
14
+ key :_api_key, String
15
+ timestamps!
16
+
17
+ # == Indices
18
+
19
+ ensure_index :email
20
+
21
+ # == Validations
22
+
23
+ validates_presence_of :name
24
+ validates_presence_of :role
25
+ validate :validate_role
26
+
27
+ VALID_ROLES = %w(basic curator admin)
28
+
29
+ def validate_role
30
+ unless VALID_ROLES.include?(role)
31
+ errors.add(:role, "must be in #{VALID_ROLES.inspect}")
32
+ end
33
+ end
34
+
35
+ # == Callbacks
36
+
37
+ def before_create
38
+ unless _api_key
39
+ self._api_key = generate_api_key
40
+ end
41
+ end
42
+
43
+ def generate_api_key
44
+ salt = Config.environment_config["api_key_salt"]
45
+ s = "#{Time.now.to_f}#{salt}#{rand(100_000_000)}#{name}#{email}"
46
+ Digest::SHA1.hexdigest(s)
47
+ end
48
+
49
+ end
50
+
51
+ end
@@ -0,0 +1,32 @@
1
+ module DataCatalog
2
+
3
+ class Categories < Base
4
+ include Resource
5
+
6
+ model Category
7
+
8
+ # == Permissions
9
+
10
+ roles Roles
11
+ permission :read => :basic
12
+ permission :modify => :curator
13
+
14
+ # == Properties
15
+
16
+ property :name
17
+
18
+ property :sources do |category|
19
+ category.sources.map do |source|
20
+ {
21
+ "id" => source.id,
22
+ "href" => "/sources/#{source.id}",
23
+ "title" => source.title,
24
+ "url" => source.url,
25
+ }
26
+ end
27
+ end
28
+ end
29
+
30
+ Categories.build
31
+
32
+ end
@@ -0,0 +1,35 @@
1
+ module DataCatalog
2
+
3
+ class Sources < Base
4
+ include Resource
5
+
6
+ model Source
7
+
8
+ # == Permissions
9
+
10
+ roles Roles
11
+ permission :read => :basic
12
+ permission :modify => :curator
13
+
14
+ # == Properties
15
+
16
+ property :title
17
+ property :url
18
+ property :raw, :w => :admin
19
+
20
+ property :categories do |source|
21
+ source.categorizations.map do |categorization|
22
+ {
23
+ "id" => categorization.category.id,
24
+ "href" => "/categories/#{categorization.category.id}",
25
+ "name" => categorization.category.name,
26
+ }
27
+ end
28
+ end
29
+
30
+ # == Callbacks
31
+ end
32
+
33
+ Sources.build
34
+
35
+ end
@@ -0,0 +1,26 @@
1
+ module DataCatalog
2
+
3
+ class Users < Base
4
+ include Resource
5
+
6
+ model User
7
+
8
+ # == Permissions
9
+
10
+ roles Roles
11
+ permission :read => :basic
12
+ permission :modify => :owner
13
+
14
+ # == Properties
15
+
16
+ property :name, :r => :basic
17
+ property :email, :r => :owner
18
+ property :role, :r => :owner, :w => :admin
19
+ property :_api_key, :r => :owner, :w => :admin
20
+
21
+ # == Callbacks
22
+ end
23
+
24
+ Users.build
25
+
26
+ end
@@ -0,0 +1,29 @@
1
+ namespace :db do
2
+
3
+ def verbosely_drop_database
4
+ db_name = Config.drop_database
5
+ puts "Dropped database: #{db_name}."
6
+ end
7
+
8
+ desc 'Drop database for current environment (development unless RACK_ENV is set)'
9
+ task :reset do
10
+ verbosely_drop_database
11
+ end
12
+
13
+ namespace :reset do
14
+ desc 'Drop databases defined in config/config.yml'
15
+ task :all do
16
+ Config.environments.each do |env_name|
17
+ Config.environment = env_name
18
+ verbosely_drop_database
19
+ end
20
+ end
21
+
22
+ desc 'Drop test database'
23
+ task :test do
24
+ Config.environment = 'test'
25
+ verbosely_drop_database
26
+ end
27
+ end
28
+
29
+ end
@@ -0,0 +1,16 @@
1
+ desc "Run tests"
2
+ task :test => %w(db:reset:test test:models test:resources)
3
+
4
+ namespace :test do
5
+
6
+ desc "Run model tests"
7
+ Rake::TestTask.new(:models) do |t|
8
+ t.test_files = FileList["test/models/*_test.rb"]
9
+ end
10
+
11
+ desc "Run other tests"
12
+ Rake::TestTask.new(:resources) do |t|
13
+ t.test_files = FileList["test/resources/**/*_test.rb"]
14
+ end
15
+
16
+ end
@@ -0,0 +1,17 @@
1
+ module Test
2
+ module Unit
3
+ module Assertions
4
+
5
+ def assert_include(expected, actual, message = nil)
6
+ _wrap_assertion do
7
+ full_message = build_message(message,
8
+ "<?> expected to include\n<?>.\n", actual, expected)
9
+ assert_block(full_message) do
10
+ actual.include?(expected)
11
+ end
12
+ end
13
+ end
14
+
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ module Test
2
+ module Unit
3
+ module Assertions
4
+
5
+ def assert_not_include(expected, actual, message = nil)
6
+ _wrap_assertion do
7
+ full_message = build_message(message,
8
+ "<?> expected to not include\n<?>.\n", actual, expected)
9
+ assert_block(full_message) do
10
+ not actual.include?(expected)
11
+ end
12
+ end
13
+ end
14
+
15
+ end
16
+ end
17
+ end