modulation 0.6

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 (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: []