modulation 0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +277 -0
  3. data/lib/modulation.rb +234 -0
  4. data/lib/modulation/gem.rb +16 -0
  5. metadata +58 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 920b7e8e5d77f1c94944970aec2cda441f7dfadb
4
+ data.tar.gz: e7422d6abea4eb6b88c4435f6fd4a1dab1e86018
5
+ SHA512:
6
+ metadata.gz: 70a95296ebf8feecbbe2361cbd8e021145d8cbad9fb6e7651acf72001bcf50f26dc33ee27fe309c04a711f050e2cd96ade41f5d0416c10b8c845b91eee7c5615
7
+ data.tar.gz: 85774eec2d3b3b34f48fffacad087ef2cd15dca5e346e3402732f9bfb7866237ba47991119138119eefe8250833331abbe7f6def0d9799f6dd29aec8d133f263
data/README.md ADDED
@@ -0,0 +1,277 @@
1
+ # Modulation - explicit dependencies for Ruby
2
+
3
+ Modulation provides an alternative way to organize Ruby code. Instead of
4
+ littering the global namespace with classes and modules, Mrodulation lets you
5
+ explicitly import and export declarations in order to better control
6
+ dependencies in your codebase.
7
+
8
+ With Modulation, you always know where a module comes from, and you have full
9
+ control over which parts of a module's code you wish to expose to the outside
10
+ world. With Modulation, you can more easily write in a functional style with a
11
+ minimum of boilerplate code.
12
+
13
+ > **Important notice**: Modulation is currently at an experimental stage. Use
14
+ > it at your own risk!
15
+
16
+ ## Rationale
17
+
18
+ Splitting your Ruby code into multiple files loaded using `require` poses a
19
+ number of problems:
20
+
21
+ - Once a file is `require`d, any class, module or constant in it is available
22
+ to any other file in your codebase. All "globals" (classes, modules,
23
+ constants) are loaded, well, globally, in a single namespace. Namespace
24
+ collisions are easy in Ruby.
25
+ - Since a `require` can appear in any file in your code, it's easy to lose
26
+ track of where a certain file was required and where it is used.
27
+ - To avoid class name ocnflicts, classes need to be nested under a single
28
+ hierarchical tree, sometime reaching 4 levels or more, i.e.
29
+ `ActiveSupport::Messages::Rotator::Encryptor`.
30
+ - There's no easy way to control the visibility of specific so-called globals.
31
+ Everything is wide-open.
32
+ - Writing reusable functional code requires wrapping it in modules using
33
+ `class << self`, `def self.foo ...` or `include Singleton`.
34
+
35
+ Personally, I have found that managing dependencies with `require` over in
36
+ large codebases is... not as elegant or painfree as I would expect from a
37
+ first-class development environment.
38
+
39
+ So I came up with Modulation, a small gem that takes a different approach to
40
+ organizing Ruby code: any so-called global declarations are hidden unless
41
+ explicitly exported, and the global namespace remains clutter-free. All
42
+ dependencies between source files are explicit, and are easily grokked.
43
+
44
+ Here's a simple example:
45
+
46
+ *math.rb*
47
+ ```ruby
48
+ export :fib
49
+
50
+ def fib(n)
51
+ (0..1).include?(n) ? n : (fib(n - 1) + fib(n - 2))
52
+ end
53
+ ```
54
+ *app.rb*
55
+ ```ruby
56
+ require 'modulation'
57
+ Math = import('./math')
58
+ puts Math.fib(10)
59
+ ```
60
+
61
+ ## Organizing Ruby code base with Modul
62
+
63
+ Any Ruby source file can be a module. Modules can export declarations (usually
64
+ an API for a specific functionality) to be shared with other modules. Modules
65
+ can also import declarations from other modules.
66
+
67
+ Each module is loaded and evaluated in the context of a newly-created `Module`,
68
+ then transformed into a class and handed off to the importing module.
69
+
70
+ ### Exporting declarations
71
+
72
+ Any class, module or constant be exported using `export`:
73
+
74
+ ```ruby
75
+ export :User, :Session
76
+
77
+ class User
78
+ ...
79
+ end
80
+
81
+ class Session
82
+ ...
83
+ end
84
+ ```
85
+
86
+ A module may also expose a set of methods without using `class << self`, for
87
+ example when writing in a functional style:
88
+
89
+ *seq.rb*
90
+ ```ruby
91
+ export :fib, :luc
92
+
93
+ def fib(n)
94
+ (0..1).include?(n) ? n : (fib(n - 1) + fib(n - 2))
95
+ end
96
+
97
+ def luc(n)
98
+ (0..1).include?(n) ? (2 - n) : (luc(n - 1) + luc(n - 2))
99
+ end
100
+ ```
101
+ *app.rb*
102
+ ```ruby
103
+ require 'modulation'
104
+ Seq = import('./seq')
105
+ puts Seq.fib(10)
106
+ ```
107
+
108
+ ### Importing declarations
109
+
110
+ Declarations from another module can be imported using `import`:
111
+
112
+ ```ruby
113
+ require 'modulation'
114
+ Models = import('./models')
115
+ ...
116
+
117
+ user = Models::User.new(...)
118
+
119
+ ...
120
+ ```
121
+
122
+ Alternatively, a module interested in a single declaration from another module
123
+ can use the following technique:
124
+
125
+ ```ruby
126
+ require 'modulation'
127
+ User = import('./models')::User
128
+ ...
129
+
130
+ user = User.new(...)
131
+ ```
132
+
133
+ > **Note about paths**: module paths are always relative to the file
134
+ > calling the `import` method.
135
+
136
+ ### Default exports
137
+
138
+ A module may wish to expose just a single class or constant, in which case it
139
+ can use `export_default`:
140
+
141
+ *user.rb*
142
+ ```ruby
143
+ export_default :User
144
+
145
+ class User
146
+ ...
147
+ end
148
+ ```
149
+
150
+ *app.rb*
151
+ ```ruby
152
+ require 'modulation'
153
+ User = import('./user')
154
+ User.new(...)
155
+ ```
156
+
157
+ The default exported value can also be defined directly thus:
158
+
159
+ *config.rb*
160
+ ```ruby
161
+ export_default(
162
+ host: 'localhost',
163
+ port: 1234,
164
+ ...
165
+ )
166
+ ```
167
+
168
+ *app.rb*
169
+ ```ruby
170
+ config = import('./config')
171
+ db.connect(config[:host], config[:port])
172
+ ```
173
+
174
+ ### Importing methods into classes and modules
175
+
176
+ Modulation provides the `extend_from` and `include_from` methods to include
177
+ imported methods in classes and modules:
178
+
179
+ ```ruby
180
+ module Sequences
181
+ extend_from('./seq.rb')
182
+ end
183
+
184
+ Sequences.fib(5)
185
+
186
+ # extend integers
187
+ class Integer
188
+ include_from('./seq.rb')
189
+
190
+ def seq(kind)
191
+ send(kind, self)
192
+ end
193
+ end
194
+
195
+ 5.seq(:fib)
196
+ ```
197
+
198
+ ### Accessing the global namespace
199
+
200
+ If you need to access the global namespace inside a module just prefix the
201
+ class name with double colons:
202
+
203
+ ```ruby
204
+ class ::GlobalClass
205
+ ...
206
+ end
207
+
208
+ ::ENV = { ... }
209
+
210
+ what = ::MEANING_OF_LIFE
211
+ ```
212
+
213
+ ## Writing gems using Modulation
214
+
215
+ Modulation can be used to write gems, providing fine-grained control over your
216
+ gem's public APIs and letting you hide any implementation details. In order to
217
+ allow loading a gem using either `require` or `import`, code your gem's main
218
+ file normally, but add `require 'modulation/gem'` at the top, and export your
219
+ gem's main namespace as a default export, e.g.:
220
+
221
+ ```ruby
222
+ require 'modulation/gem'
223
+
224
+ export_default :MyGem
225
+
226
+ module MyGem
227
+ ...
228
+ MyClass = import('my_gem/my_class')
229
+ ...
230
+ end
231
+ ```
232
+
233
+ ## Importing gems using Modulation
234
+
235
+ Gems written using modulation can also be loaded using `import`. If modulation
236
+ does not find the module specified by the given relative path, it will attempt
237
+ to load a gem by the same name.
238
+
239
+ > **Note**: using `import` to load a gem is very much *alpha*, and might
240
+ > introduce problems not encountered when loading with `require` such as
241
+ > shadowing of global namespaces, or any other bizarre and unexpected
242
+ > behaviors. Actually, there's not much point in using it to load a gem which
243
+ > does not use Modulation. When loading gems using import, Modulation will
244
+ > raise an exception if no symbols were exported by the gem.
245
+
246
+ ## Coding style recommendations
247
+
248
+ * Import modules into constants, not into variables:
249
+
250
+ ```ruby
251
+ Settings = import('./settings')
252
+ ```
253
+
254
+ * Place your exports at the top of your module:
255
+
256
+ ```ruby
257
+ export :foo, :bar, :baz
258
+
259
+ ...
260
+ ```
261
+
262
+ * Place your imports at the top of your module:
263
+
264
+ ```ruby
265
+ Foo = import('./foo')
266
+ Bar = import('./bar')
267
+ Baz = import('./baz')
268
+
269
+ ...
270
+ ```
271
+
272
+ ## Known limitations and problems
273
+
274
+ - Modulation is (probably) not production-ready.
275
+ - Modulation probably doesn't play well with `Marshal`.
276
+ - Modulation probably doesn't play well with code-analysis tools.
277
+ - Modulation doesn't play well with rdoc/yard.
data/lib/modulation.rb ADDED
@@ -0,0 +1,234 @@
1
+ require 'fileutils'
2
+ # frozen_string_literal: true
3
+
4
+ # Kernel extensions - modul's API
5
+ module Kernel
6
+ # Returns an encapsulated imported module.
7
+ # @param fn [String] module file name
8
+ # @param caller_location [String] caller location
9
+ # @return [Class] module facade
10
+ def import(fn, caller_location = caller.first)
11
+ Modulation.import_module(fn, caller_location)
12
+ end
13
+ end
14
+
15
+ # Object extensions
16
+ class Object
17
+ # Returns the objects metaclass (shamelessly stolen from the metaid gem).
18
+ # @return [Class] object's metaclass
19
+ def metaclass; class << self; self; end; end
20
+ end
21
+
22
+ class Module
23
+ # Extends the receiver with exported methods from the given file name
24
+ # @param fn [String] module filename
25
+ # @return [void]
26
+ def extend_from(fn)
27
+ mod = import(fn, caller.first)
28
+ mod.methods(false).each do |sym|
29
+ metaclass.send(:define_method, sym, mod.method(sym).to_proc)
30
+ end
31
+ end
32
+
33
+ # Includes exported methods from the given file name in the receiver
34
+ # The module's methods will be available as instance methods
35
+ # @param fn [String] module filename
36
+ # @return [void]
37
+ def include_from(fn)
38
+ mod = import(fn, caller.first)
39
+ mod.methods(false).each do |sym|
40
+ send(:define_method, sym, mod.method(sym).to_proc)
41
+ end
42
+ end
43
+ end
44
+
45
+ class Modulation
46
+ @@loaded_modules = {}
47
+ @@full_backtrace = false
48
+
49
+ def self.full_backtrace!
50
+ @@full_backtrace = true
51
+ end
52
+
53
+ # Imports a module from a file
54
+ # If the module is already loaded, returns the loaded module.
55
+ # @param fn [String] source file name (with or without extension)
56
+ # @param caller_location [String]
57
+ # @return [Module] loaded module object
58
+ def self.import_module(fn, caller_location = caller.first)
59
+ fn = module_absolute_path(fn, caller_location)
60
+ @@loaded_modules[fn] ||= create_module_from_file(fn)
61
+ end
62
+
63
+ def self.module_absolute_path(fn, caller_location)
64
+ orig_fn = fn
65
+ caller_file = (caller_location =~ /^([^\:]+)\:/) ?
66
+ $1 : (raise "Could not expand path")
67
+ fn = File.expand_path(fn, File.dirname(caller_file))
68
+ if File.file?("#{fn}.rb")
69
+ fn + '.rb'
70
+ else
71
+ if File.file?(fn)
72
+ return fn
73
+ else
74
+ lookup_gem(orig_fn) || (raise "Module not found: #{fn}")
75
+ end
76
+ end
77
+ end
78
+
79
+ def self.lookup_gem(name)
80
+ spec = Gem::Specification.find_by_name(name)
81
+ fn = File.join(spec.full_require_paths, "#{name}.rb")
82
+ File.file?(fn) ? fn : nil
83
+ rescue Gem::MissingSpecError
84
+ nil
85
+ end
86
+
87
+ # Creates a new module from a source file
88
+ # @param fn [String] source file name
89
+ # @return [Module] module
90
+ def self.create_module_from_file(fn)
91
+ make_module(location: fn)
92
+ rescue => e
93
+ if @@full_backtrace
94
+ raise
95
+ else
96
+ # remove *modul* methods from backtrace and reraise
97
+ backtrace = e.backtrace.reject {|l| l.include?(__FILE__)}
98
+ raise(e, e.message, backtrace)
99
+ end
100
+ end
101
+
102
+ # Loads a module from file or block, wrapping it in a module facade
103
+ # @param info [Hash] module info
104
+ # @param block [Proc] module block
105
+ # @return [Class] module facade
106
+ def self.make_module(info, &block)
107
+ export_default = nil
108
+ m = initialize_module {|v| export_default = v}
109
+ m.__module_info = info
110
+ load_module_code(m, info, &block)
111
+
112
+ if export_default
113
+ transform_export_default_value(export_default, m)
114
+ else
115
+ m.tap {m.__set_exported_symbols(m, m.__exported_symbols)}
116
+ end
117
+ end
118
+
119
+ # Returns exported value for a default export
120
+ # If the given value is a symbol, returns the value of the corresponding
121
+ # constant.
122
+ # @param value [any] export_default value
123
+ # @param mod [Module] module
124
+ # @return [any] exported value
125
+ def self.transform_export_default_value(value, mod)
126
+ if value.is_a?(Symbol) && mod.metaclass.const_defined?(value)
127
+ mod.metaclass.const_get(value)
128
+ else
129
+ value
130
+ end
131
+ end
132
+
133
+ # Initializes a new module ready to evaluate a file module
134
+ # @note The given block is used to pass the value given to `export_default`
135
+ # @return [Module] new module
136
+ def self.initialize_module(&export_default_block)
137
+ Module.new.tap do |m|
138
+ m.extend(ModuleMethods)
139
+ m.metaclass.include(ModuleMetaclassMethods)
140
+ m.__export_default_block = export_default_block
141
+ end
142
+ end
143
+
144
+ # Loads a source file or a block into the given module
145
+ # @param m [Module] module
146
+ # @param fn [String] source file path
147
+ # @return [void]
148
+ def self.load_module_code(m, info)
149
+ fn = info[:location]
150
+ m.instance_eval(IO.read(fn), fn)
151
+ end
152
+
153
+ # Module façade methods
154
+ module ModuleMethods
155
+ # Responds to missing constants by checking metaclass
156
+ # If the given constant is defined on the metaclass, the same constant is
157
+ # defined on self and its value is returned. This is essential to
158
+ # supporting constants in modules.
159
+ # @param name [Symbol] constant name
160
+ # @return [any] constant value
161
+ def const_missing(name)
162
+ if metaclass.const_defined?(name)
163
+ unless !@__exported_symbols || @__exported_symbols.include?(name)
164
+ raise NameError, "private constant `#{name}' accessed in #{inspect}", caller
165
+ end
166
+ metaclass.const_get(name).tap {|value| const_set(name, value)}
167
+ else
168
+ raise NameError, "uninitialized constant #{inspect}::#{name}", caller
169
+ end
170
+ end
171
+
172
+ # read and write module information
173
+ attr_accessor :__module_info
174
+
175
+ # Sets exported_symbols ivar and marks all non-exported methods as private
176
+ # @param m [Module] imported module
177
+ # @param symbols [Array] array of exported symbols
178
+ # @return [void]
179
+ def __set_exported_symbols(m, symbols)
180
+ @__exported_symbols = symbols
181
+ metaclass.instance_methods(false).each do |m|
182
+ metaclass.send(:private, m) unless symbols.include?(m)
183
+ end
184
+ end
185
+
186
+ # Returns a text representation of the module for inspection
187
+ # @return [String] module string representation
188
+ def inspect
189
+ module_name = name || 'Module'
190
+ if __module_info[:location]
191
+ "#{module_name}:#{__module_info[:location]}"
192
+ else
193
+ "#{module_name}"
194
+ end
195
+ end
196
+ end
197
+
198
+ # Module façade metaclass methods
199
+ module ModuleMetaclassMethods
200
+ # Adds given symbols to the exported_symbols array
201
+ # @param symbols [Array] array of symbols
202
+ # @return [void]
203
+ def export(*symbols)
204
+ symbols = symbols.first if Array === symbols.first
205
+ __exported_symbols.concat(symbols)
206
+ end
207
+
208
+ # Sets a module's value, so when imported it will represent the given value,
209
+ # instead of a module facade
210
+ # @param v [Symbol, any] symbol or value
211
+ # @return [void]
212
+ def export_default(v)
213
+ @__export_default_block.call(v) if @__export_default_block
214
+ end
215
+
216
+ # read and write module info
217
+ attr_accessor :__module_info
218
+
219
+ # Returns exported_symbols array
220
+ # @return [Array] array of exported symbols
221
+ def __exported_symbols
222
+ @exported_symbols ||= []
223
+ end
224
+
225
+ # Sets export_default block, used for setting the returned module object to
226
+ # a class or constant
227
+ # @param block [Proc] default export block
228
+ # @return [void]
229
+ def __export_default_block=(block)
230
+ @__export_default_block = block
231
+ end
232
+ end
233
+ end
234
+
@@ -0,0 +1,16 @@
1
+ require_relative('../modulation')
2
+
3
+ # Kernel extensions - mock up the Modulation API with nop methods, so
4
+ # requiring a gem would work. Sample usage:
5
+ #
6
+ # require 'modulation/gem'
7
+ # export_default :MyGem
8
+ #
9
+ # module MyGem
10
+ # MyClass = import('my_class')
11
+ # MyOtherClass = import('my_other_class')
12
+ # end
13
+ module Kernel
14
+ def export(*args); end
15
+ def export_default(v); end
16
+ end
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: modulation
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.6'
5
+ platform: ruby
6
+ authors:
7
+ - Sharon Rosner
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-07-23 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: "Modulation provides an alternative way to organize Ruby code. Instead
14
+ of \nlittering the global namespace with classes and modules, Modulation lets\nyou
15
+ explicitly import and export declarations in order to better control \ndependencies
16
+ in your codebase.\n\nWith Modulation, you always know where a module comes from,
17
+ and you have\nfull control over which parts of a module's code you wish to expose
18
+ to the \noutside world. With Modulation, you can more easily write in a functional\nstyle
19
+ with a minimum of boilerplate code.\n"
20
+ email: ciconia@gmail.com
21
+ executables: []
22
+ extensions: []
23
+ extra_rdoc_files:
24
+ - README.md
25
+ files:
26
+ - README.md
27
+ - lib/modulation.rb
28
+ - lib/modulation/gem.rb
29
+ homepage: http://github.com/ciconia/modulation
30
+ licenses:
31
+ - MIT
32
+ metadata:
33
+ source_code_uri: https://github.com/ciconia/modulation
34
+ post_install_message:
35
+ rdoc_options:
36
+ - "--title"
37
+ - Modulation
38
+ - "--main"
39
+ - README.md
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ requirements: []
53
+ rubyforge_project:
54
+ rubygems_version: 2.6.13
55
+ signing_key:
56
+ specification_version: 4
57
+ summary: 'Modulation: explicit dependencies for Ruby'
58
+ test_files: []