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 CHANGED
@@ -1,18 +1,24 @@
1
- Namespaces for ruby <hack>
2
- ==========================
1
+ Namespaces for ruby
2
+ ===================
3
3
 
4
- Brings a namespace system to ruby. By loading the .rb files into a temporary
5
- module, it is possible to avoid polluting the global namespace and select what to import
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 system, but it was adapted
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
@@ -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
- include Namespace
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
- # Loads a file according to the +namespace+ in the context of a module,
26
- # and returns that object.
27
- #
28
- # It's not a Python import, where the namespace is available. What you
29
- # get here, is a sort of Sandbox where constants defined in the file are
30
- # not polluting the global namespace but available on the returned object.
31
- # Since the returned object is a Module, it can be included in the current
32
- # module if you're also in the contect of a "Sandbox".
33
- #
34
- # #import has been chosen because the ruby namespace is already crowded
35
- # (require, load and include are already taken)
36
- #
37
- def import(namespace)
38
- namespace = namespace.to_s.gsub(/[\/\\:]+/, ':') # allow symbols
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
- # @__namespace__ is not available outside of the ruby top-level
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
- # Cache lookup
50
- ns = Namespace::cache[namespace]
51
- return ns if ns
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
- file = nil
54
- # File lookup
55
- file_path = namespace[1..-1].gsub(':', File::SEPARATOR) + '.rb'
56
- $LOAD_PATH.each do |path|
57
- path = File.join(path, file_path)
58
- if File.exists? path
59
- file = path
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
- raise LoadError, "no such file to load -- #{file_path}" unless file
51
+ # Cache
52
+ @cache[namespace] = ns
65
53
 
66
- file_content = File.read(file_path)
67
- # Pre-process __NAMESPACE__ keyword to act like __FILE__
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
- # Make sure NS is new (in the cases where the current NS requires another NS)
72
- Object.send(:remove_const, :NS) rescue nil
73
- # String is on 1 line to make sure line-errors are preserved
74
- ns = eval("module NS; @__namespace__ = #{namespace.inspect}; include Namespace::Def; #{file_content}; self; end", TOPLEVEL_BINDING, file)
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
- # Cache
77
- Namespace::cache[namespace] = ns
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
- return ns
80
- ensure
81
- Object.send(:remove_const, :NS) rescue nil
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
- end
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
- # Make #import available to all
86
- class Object; include Namespace; end
89
+ mod
90
+ end
87
91
 
88
- if __FILE__ == $0
89
- require 'irb'
90
- require 'irb/completion'
91
- IRB.start
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
@@ -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
@@ -0,0 +1,4 @@
1
+
2
+ def b; "b"; end
3
+
4
+ class B; end
@@ -0,0 +1,4 @@
1
+ # this should return module b
2
+ def return_b
3
+ import('b')
4
+ end
@@ -0,0 +1,5 @@
1
+ # Used to test name conflict
2
+
3
+ def a; "foo"; end
4
+
5
+ import :a
@@ -0,0 +1,2 @@
1
+
2
+ def dir; "dir"; end
@@ -0,0 +1,2 @@
1
+ $:.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'namespace'
@@ -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
- - 0
9
- version: "1.0"
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: 2010-11-20 00:00:00 +00:00
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 a hack that allows you to load specific
23
- ruby files in the context of a Module, thus avoiding global namespace
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: Bringing namespaces to Ruby
72
+ summary: Ruby namespaces experiment
69
73
  test_files: []
70
74