namespace 1.0 → 1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +31 -8
- data/lib/namespace.rb +83 -73
- data/test/fixtures/a.rb +18 -0
- data/test/fixtures/b.rb +4 -0
- data/test/fixtures/dyn_import.rb +4 -0
- data/test/fixtures/name_conflict.rb +5 -0
- data/test/fixtures/sub/dir.rb +2 -0
- data/test/helper.rb +2 -0
- data/test/test.rb +78 -0
- metadata +13 -9
data/README.md
CHANGED
@@ -1,18 +1,24 @@
|
|
1
|
-
Namespaces for ruby
|
2
|
-
|
1
|
+
Namespaces for ruby
|
2
|
+
===================
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
in the current module.
|
4
|
+
It is an experiment to bring namespaces to ruby. It is not quite clear yet
|
5
|
+
why this would be useful, but it can be interesting nonetheless.
|
7
6
|
|
8
|
-
This system is inspired by the CommonJS module
|
7
|
+
This system is inspired by the CommonJS and Python module systems, adapted
|
9
8
|
for the ruby particularities.
|
10
9
|
|
10
|
+
|
11
|
+
By loading the .rb files into a temporary
|
12
|
+
module, it is possible to avoid polluting the global namespace and select what to import
|
13
|
+
in the current module.
|
14
|
+
|
11
15
|
Feature:
|
16
|
+
|
17
|
+
* Compatible with ruby 1.8.7 and 1.9.2 (and others?)
|
12
18
|
* Possible to avoid namespace collision on the module level
|
13
|
-
* Relative requires (if import is used in the top-level of the file)
|
14
19
|
|
15
20
|
Unfeatures:
|
21
|
+
|
16
22
|
* uses eval and other nasty ruby hacks
|
17
23
|
* classes and namespaces don't mix well because "class X; <<here>>; end"
|
18
24
|
is a new context that doesn't inherit from it's parent.
|
@@ -23,10 +29,27 @@ TODO
|
|
23
29
|
* Handle circular dependencies ?
|
24
30
|
* Avoid eval (but I don't see how)
|
25
31
|
|
32
|
+
Tricks
|
33
|
+
------
|
34
|
+
|
35
|
+
A module can emulate the main object behavior by extending itself. In that way,
|
36
|
+
it makes it's functions directly available to consumption.
|
37
|
+
|
38
|
+
module X
|
39
|
+
extend self
|
40
|
+
def test
|
41
|
+
"x"
|
42
|
+
end
|
43
|
+
test #=> "x"
|
44
|
+
end
|
45
|
+
|
46
|
+
TODO
|
47
|
+
|
26
48
|
Bikeshed
|
27
49
|
--------
|
28
50
|
|
29
51
|
* Should ruby "namespaced" modules have their own $LOAD_PATH and file extensions ?
|
52
|
+
*
|
30
53
|
|
31
54
|
Licence
|
32
55
|
-------
|
@@ -34,4 +57,4 @@ Licence
|
|
34
57
|
Public domain
|
35
58
|
|
36
59
|
Cheers,
|
37
|
-
Jonas
|
60
|
+
Jonas
|
data/lib/namespace.rb
CHANGED
@@ -1,92 +1,102 @@
|
|
1
1
|
# Namespaces for ruby.
|
2
2
|
#
|
3
|
-
# See Namespace#import
|
3
|
+
# See Namespace.open and Namespace#import for more details
|
4
4
|
module Namespace
|
5
5
|
@cache = {}
|
6
|
+
@load_path = $LOAD_PATH
|
6
7
|
class << self;
|
7
8
|
attr_reader :cache
|
8
|
-
|
9
|
-
end
|
10
|
-
|
11
|
-
module Def
|
12
|
-
def self.included(mod)
|
13
|
-
eigenclass = (class << mod
|
14
|
-
# Override to not show Namespace::NS for all modules
|
15
|
-
def inspect
|
16
|
-
defined?(@__namespace__) ? "<#{@__namespace__} #{(constants+instance_methods).sort.join(', ')}>" : super
|
17
|
-
end
|
18
|
-
self
|
19
|
-
end)
|
20
|
-
eigenclass.send(:include, mod, Namespace)
|
21
|
-
end
|
22
|
-
end
|
9
|
+
attr_reader :load_path
|
23
10
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
11
|
+
# Loads a file according to the +namespace+ in the context of a module,
|
12
|
+
# and returns that object.
|
13
|
+
#
|
14
|
+
# It's not a Python import, where the namespace is available. What you
|
15
|
+
# get here, is a sort of Sandbox where constants defined in the file are
|
16
|
+
# not polluting the global namespace but available on the returned object.
|
17
|
+
# Since the returned object is a Module, it can be included in the current
|
18
|
+
# module if you're also in the context of a "Sandbox".
|
19
|
+
#
|
20
|
+
# #import has been chosen because the ruby namespace is already crowded
|
21
|
+
# (require, load and include are already taken)
|
22
|
+
def open(namespace)
|
23
|
+
namespace = normalize(namespace)
|
24
|
+
|
25
|
+
# Cache lookup
|
26
|
+
ns = @cache[namespace]
|
27
|
+
return ns if ns
|
28
|
+
|
29
|
+
# File lookup
|
30
|
+
file_path = namespace.gsub(':', File::SEPARATOR) + '.rb'
|
31
|
+
file = @load_path.inject(nil) do |file, load_path|
|
32
|
+
path = File.join(load_path, file_path)
|
33
|
+
File.exists?(path) ? path : file
|
34
|
+
end
|
39
35
|
|
40
|
-
|
41
|
-
if defined?(@__namespace__) && namespace[0..0] != ":"
|
42
|
-
# expand namespace
|
43
|
-
namespace = @__namespace__.sub(/:[^:]+$/,'') + ':' + namespace
|
44
|
-
else
|
45
|
-
namespace = ':' + namespace
|
46
|
-
end
|
47
|
-
puts "ns##{namespace}"
|
36
|
+
raise ImportError, "no such file to load -- #{file_path}" unless file
|
48
37
|
|
49
|
-
|
50
|
-
|
51
|
-
|
38
|
+
file_content = File.read(file)
|
39
|
+
# Pre-process __NAMESPACE__ keyword to act like __FILE__
|
40
|
+
# in know... it's not exactly the same... (eg "__FILE__") but it should do the trick
|
41
|
+
file_content.gsub!('__NAMESPACE__', namespace.inspect)
|
52
42
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
break
|
61
|
-
end
|
62
|
-
end
|
43
|
+
ns = Module.new
|
44
|
+
ns.instance_variable_set("@__namespace__", namespace)
|
45
|
+
# Allow calling methods in self, like in global namespace
|
46
|
+
ns.extend(ns)
|
47
|
+
# Adds the #import methods from the DSL
|
48
|
+
ns.extend(Namespace, Namespace::Ext)
|
49
|
+
ns.module_eval(file_content, file)
|
63
50
|
|
64
|
-
|
51
|
+
# Cache
|
52
|
+
@cache[namespace] = ns
|
65
53
|
|
66
|
-
|
67
|
-
|
68
|
-
# in know... it's not exactly the same... (eg "__FILE__") but it should do the trick
|
69
|
-
file_content.gsub!('__NAMESPACE__', namespace.inspect)
|
54
|
+
return ns
|
55
|
+
end
|
70
56
|
|
71
|
-
#
|
72
|
-
|
73
|
-
|
74
|
-
|
57
|
+
# Transforms a string into a normalized namespace identifier
|
58
|
+
def normalize(namespace)
|
59
|
+
namespace.to_s.sub(/^[\/:\s]*/, '').sub(/[\/:\s]*$/, '').gsub(/[\/\\:]+/, '::')
|
60
|
+
end
|
75
61
|
|
76
|
-
#
|
77
|
-
|
62
|
+
# Returns the top name of a namespace
|
63
|
+
#
|
64
|
+
# Example: basename("some::deep::namespace") => "namespace"
|
65
|
+
def basename(namespace)
|
66
|
+
normalize(namespace).gsub(/.*::/,'')
|
67
|
+
end
|
78
68
|
|
79
|
-
|
80
|
-
|
81
|
-
|
69
|
+
# Raises a ScriptError if the module is included
|
70
|
+
def included(mod)
|
71
|
+
raise ScriptError, "Namespace is not designed to be included, only extended. See #{mod}"
|
72
|
+
end
|
73
|
+
private :included
|
82
74
|
end
|
83
|
-
|
75
|
+
|
76
|
+
|
77
|
+
# options[:as] = select the imported name
|
78
|
+
#
|
79
|
+
# THINK: since variable is already returned, is :as necessary?
|
80
|
+
def import(namespace, options={})
|
81
|
+
mod = Namespace::open(namespace)
|
82
|
+
|
83
|
+
name = options[:as] || options['as'] || Namespace.basename(namespace)
|
84
|
+
raise ConflictError, "name `#{name}` is already taken" if method_defined?(name)
|
85
|
+
instance_variable_set("@#{name}", mod)
|
86
|
+
attr_reader name
|
87
|
+
private name
|
84
88
|
|
85
|
-
|
86
|
-
|
89
|
+
mod
|
90
|
+
end
|
87
91
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
+
# Used to override #inspect and #to_s of a namespace
|
93
|
+
module Ext
|
94
|
+
attr_reader :__namespace__
|
95
|
+
alias inspect __namespace__
|
96
|
+
alias to_s __namespace__
|
97
|
+
private :__namespace__
|
98
|
+
end
|
99
|
+
|
100
|
+
class ImportError < ScriptError; end
|
101
|
+
class ConflictError < ScriptError; end
|
92
102
|
end
|
data/test/fixtures/a.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
def assert(cond, msg=nil)
|
2
|
+
raise AssertionError, (msg || "Assertion Error") unless cond
|
3
|
+
end
|
4
|
+
|
5
|
+
def assert_equal(obj1, obj2)
|
6
|
+
assert(obj1 == obj2, "#{obj1} and #{obj2} should be equal")
|
7
|
+
end
|
8
|
+
|
9
|
+
import 'b'
|
10
|
+
import 'sub/dir', :as=>'foo'
|
11
|
+
|
12
|
+
assert(defined? b::B)
|
13
|
+
|
14
|
+
assert_equal("b", b::b)
|
15
|
+
assert_equal("dir", foo::dir)
|
16
|
+
|
17
|
+
def a; "a"; end
|
18
|
+
class A; end
|
data/test/fixtures/b.rb
ADDED
data/test/helper.rb
ADDED
data/test/test.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
require File.expand_path('../helper', __FILE__)
|
2
|
+
require 'test/unit'
|
3
|
+
|
4
|
+
class TestNamespace < Test::Unit::TestCase
|
5
|
+
def setup
|
6
|
+
Namespace.cache.clear
|
7
|
+
Namespace.load_path.replace([File.expand_path('../fixtures', __FILE__)])
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_basics
|
11
|
+
a = Namespace.open 'a'
|
12
|
+
assert(a.const_defined?(:A))
|
13
|
+
assert(a.method_defined?(:a))
|
14
|
+
|
15
|
+
assert(a::A.kind_of? Module)
|
16
|
+
assert_equal("a", a.a)
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_importing_namespace
|
20
|
+
m = Module.new
|
21
|
+
m.extend Namespace
|
22
|
+
ret = m.module_eval do
|
23
|
+
import(:a).class
|
24
|
+
end
|
25
|
+
assert_equal(Module, ret)
|
26
|
+
|
27
|
+
assert_raise(ScriptError) do
|
28
|
+
m.send(:include, Namespace)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_naming
|
33
|
+
a = Namespace.open 'a'
|
34
|
+
assert_equal('a', a.to_s)
|
35
|
+
assert_equal('a', a.inspect)
|
36
|
+
|
37
|
+
# THIS test is broken. If I remember well, ruby finds the module name internally
|
38
|
+
# so it can't be overridden.
|
39
|
+
# To fix this, we would have to override the #to_s, #inspect and #name
|
40
|
+
# methods of all the exported modules and classes. Not good :-/
|
41
|
+
#assert_equal('a::A', a::A.name)
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_naming_conflict
|
45
|
+
assert_raises(Namespace::ConflictError) do
|
46
|
+
Namespace::open('name_conflict')
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def test_normalization
|
51
|
+
norm_test = proc do |output, namespace|
|
52
|
+
assert_equal(output, Namespace::normalize(namespace))
|
53
|
+
end
|
54
|
+
|
55
|
+
norm_test["simple", "simple"]
|
56
|
+
norm_test["simple", :simple]
|
57
|
+
norm_test["sub::dir", "sub/dir"]
|
58
|
+
norm_test["sub::dir", "sub:dir"]
|
59
|
+
norm_test["sub::dir", "sub::dir"]
|
60
|
+
norm_test["strip", "/strip"]
|
61
|
+
norm_test["strip", "strip/"]
|
62
|
+
norm_test["strip", " ::strip/ "]
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_basename
|
66
|
+
base_test = proc do |output, namespace|
|
67
|
+
assert_equal(output, Namespace::basename(namespace))
|
68
|
+
end
|
69
|
+
base_test["simple", "simple"]
|
70
|
+
base_test["dir", "sub::dir"]
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_dynimport
|
74
|
+
a = Namespace.open 'dyn_import'
|
75
|
+
b = Namespace.open 'b'
|
76
|
+
assert_equal(a.return_b, b)
|
77
|
+
end
|
78
|
+
end
|
metadata
CHANGED
@@ -1,12 +1,11 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: namespace
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash: 15
|
5
4
|
prerelease: false
|
6
5
|
segments:
|
7
6
|
- 1
|
8
|
-
-
|
9
|
-
version: "1.
|
7
|
+
- 2
|
8
|
+
version: "1.2"
|
10
9
|
platform: ruby
|
11
10
|
authors:
|
12
11
|
- Jonas Pfenniger
|
@@ -14,13 +13,13 @@ autorequire:
|
|
14
13
|
bindir: bin
|
15
14
|
cert_chain: []
|
16
15
|
|
17
|
-
date:
|
16
|
+
date: 2011-01-05 00:00:00 +00:00
|
18
17
|
default_executable:
|
19
18
|
dependencies: []
|
20
19
|
|
21
20
|
description: |-
|
22
|
-
This module is
|
23
|
-
|
21
|
+
This module is an experiment to bring namespaces to ruby.
|
22
|
+
Each imported file is loaded in it's own context,thus avoiding global namespace
|
24
23
|
pollution
|
25
24
|
email: jonas@pfenniger.name
|
26
25
|
executables: []
|
@@ -32,6 +31,13 @@ extra_rdoc_files: []
|
|
32
31
|
files:
|
33
32
|
- README.md
|
34
33
|
- lib/namespace.rb
|
34
|
+
- test/fixtures/a.rb
|
35
|
+
- test/fixtures/b.rb
|
36
|
+
- test/fixtures/dyn_import.rb
|
37
|
+
- test/fixtures/name_conflict.rb
|
38
|
+
- test/fixtures/sub/dir.rb
|
39
|
+
- test/helper.rb
|
40
|
+
- test/test.rb
|
35
41
|
has_rdoc: true
|
36
42
|
homepage: https://github.com/zimbatm/namespace.rb
|
37
43
|
licenses: []
|
@@ -46,7 +52,6 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
46
52
|
requirements:
|
47
53
|
- - ">="
|
48
54
|
- !ruby/object:Gem::Version
|
49
|
-
hash: 3
|
50
55
|
segments:
|
51
56
|
- 0
|
52
57
|
version: "0"
|
@@ -55,7 +60,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
55
60
|
requirements:
|
56
61
|
- - ">="
|
57
62
|
- !ruby/object:Gem::Version
|
58
|
-
hash: 3
|
59
63
|
segments:
|
60
64
|
- 0
|
61
65
|
version: "0"
|
@@ -65,6 +69,6 @@ rubyforge_project:
|
|
65
69
|
rubygems_version: 1.3.7
|
66
70
|
signing_key:
|
67
71
|
specification_version: 3
|
68
|
-
summary:
|
72
|
+
summary: Ruby namespaces experiment
|
69
73
|
test_files: []
|
70
74
|
|