monad 0.0.1

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 (143) hide show
  1. data/CONTRIBUTING.md +68 -0
  2. data/Gemfile +2 -0
  3. data/LICENSE +21 -0
  4. data/README.md +88 -0
  5. data/Rakefile +136 -0
  6. data/bin/monad +102 -0
  7. data/cucumber.yml +3 -0
  8. data/features/create_sites.feature +112 -0
  9. data/features/data_sources.feature +76 -0
  10. data/features/drafts.feature +25 -0
  11. data/features/embed_filters.feature +60 -0
  12. data/features/markdown.feature +30 -0
  13. data/features/pagination.feature +54 -0
  14. data/features/permalinks.feature +65 -0
  15. data/features/post_data.feature +214 -0
  16. data/features/site_configuration.feature +206 -0
  17. data/features/site_data.feature +101 -0
  18. data/features/step_definitions/monad_steps.rb +175 -0
  19. data/features/support/env.rb +25 -0
  20. data/lib/monad.rb +90 -0
  21. data/lib/monad/command.rb +27 -0
  22. data/lib/monad/commands/build.rb +64 -0
  23. data/lib/monad/commands/doctor.rb +29 -0
  24. data/lib/monad/commands/new.rb +50 -0
  25. data/lib/monad/commands/serve.rb +33 -0
  26. data/lib/monad/configuration.rb +183 -0
  27. data/lib/monad/converter.rb +48 -0
  28. data/lib/monad/converters/identity.rb +21 -0
  29. data/lib/monad/converters/markdown.rb +43 -0
  30. data/lib/monad/converters/markdown/kramdown_parser.rb +44 -0
  31. data/lib/monad/converters/markdown/maruku_parser.rb +47 -0
  32. data/lib/monad/converters/markdown/rdiscount_parser.rb +35 -0
  33. data/lib/monad/converters/markdown/redcarpet_parser.rb +70 -0
  34. data/lib/monad/converters/textile.rb +50 -0
  35. data/lib/monad/convertible.rb +152 -0
  36. data/lib/monad/core_ext.rb +68 -0
  37. data/lib/monad/deprecator.rb +32 -0
  38. data/lib/monad/draft.rb +35 -0
  39. data/lib/monad/drivers/json_driver.rb +39 -0
  40. data/lib/monad/drivers/yaml_driver.rb +23 -0
  41. data/lib/monad/errors.rb +4 -0
  42. data/lib/monad/filters.rb +154 -0
  43. data/lib/monad/generator.rb +4 -0
  44. data/lib/monad/generators/pagination.rb +143 -0
  45. data/lib/monad/layout.rb +42 -0
  46. data/lib/monad/logger.rb +54 -0
  47. data/lib/monad/mime.types +85 -0
  48. data/lib/monad/page.rb +163 -0
  49. data/lib/monad/plugin.rb +75 -0
  50. data/lib/monad/post.rb +377 -0
  51. data/lib/monad/site.rb +455 -0
  52. data/lib/monad/static_file.rb +70 -0
  53. data/lib/monad/tags/gist.rb +30 -0
  54. data/lib/monad/tags/highlight.rb +85 -0
  55. data/lib/monad/tags/include.rb +37 -0
  56. data/lib/monad/tags/post_url.rb +61 -0
  57. data/lib/site_template/.gitignore +1 -0
  58. data/lib/site_template/_config.yml +2 -0
  59. data/lib/site_template/_layouts/default.html +46 -0
  60. data/lib/site_template/_layouts/post.html +9 -0
  61. data/lib/site_template/_posts/0000-00-00-welcome-to-monad.markdown.erb +24 -0
  62. data/lib/site_template/css/main.css +165 -0
  63. data/lib/site_template/css/syntax.css +60 -0
  64. data/lib/site_template/index.html +13 -0
  65. data/monad.gemspec +197 -0
  66. data/script/bootstrap +2 -0
  67. data/test/fixtures/broken_front_matter1.erb +5 -0
  68. data/test/fixtures/broken_front_matter2.erb +4 -0
  69. data/test/fixtures/broken_front_matter3.erb +7 -0
  70. data/test/fixtures/exploit_front_matter.erb +4 -0
  71. data/test/fixtures/front_matter.erb +4 -0
  72. data/test/fixtures/members.yaml +7 -0
  73. data/test/helper.rb +62 -0
  74. data/test/source/.htaccess +8 -0
  75. data/test/source/_includes/sig.markdown +3 -0
  76. data/test/source/_layouts/default.html +27 -0
  77. data/test/source/_layouts/simple.html +1 -0
  78. data/test/source/_plugins/dummy.rb +8 -0
  79. data/test/source/_posts/2008-02-02-not-published.textile +8 -0
  80. data/test/source/_posts/2008-02-02-published.textile +8 -0
  81. data/test/source/_posts/2008-10-18-foo-bar.textile +8 -0
  82. data/test/source/_posts/2008-11-21-complex.textile +8 -0
  83. data/test/source/_posts/2008-12-03-permalinked-post.textile +9 -0
  84. data/test/source/_posts/2008-12-13-include.markdown +8 -0
  85. data/test/source/_posts/2009-01-27-array-categories.textile +10 -0
  86. data/test/source/_posts/2009-01-27-categories.textile +7 -0
  87. data/test/source/_posts/2009-01-27-category.textile +7 -0
  88. data/test/source/_posts/2009-01-27-empty-categories.textile +7 -0
  89. data/test/source/_posts/2009-01-27-empty-category.textile +7 -0
  90. data/test/source/_posts/2009-03-12-hash-#1.markdown +6 -0
  91. data/test/source/_posts/2009-05-18-empty-tag.textile +6 -0
  92. data/test/source/_posts/2009-05-18-empty-tags.textile +6 -0
  93. data/test/source/_posts/2009-05-18-tag.textile +6 -0
  94. data/test/source/_posts/2009-05-18-tags.textile +9 -0
  95. data/test/source/_posts/2009-06-22-empty-yaml.textile +3 -0
  96. data/test/source/_posts/2009-06-22-no-yaml.textile +1 -0
  97. data/test/source/_posts/2010-01-08-triple-dash.markdown +5 -0
  98. data/test/source/_posts/2010-01-09-date-override.textile +7 -0
  99. data/test/source/_posts/2010-01-09-time-override.textile +7 -0
  100. data/test/source/_posts/2010-01-09-timezone-override.textile +7 -0
  101. data/test/source/_posts/2010-01-16-override-data.textile +4 -0
  102. data/test/source/_posts/2011-04-12-md-extension.md +7 -0
  103. data/test/source/_posts/2011-04-12-text-extension.text +0 -0
  104. data/test/source/_posts/2013-01-02-post-excerpt.markdown +14 -0
  105. data/test/source/_posts/2013-01-12-nil-layout.textile +6 -0
  106. data/test/source/_posts/2013-01-12-no-layout.textile +5 -0
  107. data/test/source/_posts/2013-03-19-not-a-post.markdown/.gitkeep +0 -0
  108. data/test/source/_posts/2013-04-11-custom-excerpt.markdown +10 -0
  109. data/test/source/_posts/2013-05-10-number-category.textile +7 -0
  110. data/test/source/_posts/es/2008-11-21-nested.textile +8 -0
  111. data/test/source/about.html +6 -0
  112. data/test/source/category/_posts/2008-9-23-categories.textile +6 -0
  113. data/test/source/contacts.html +5 -0
  114. data/test/source/contacts/bar.html +5 -0
  115. data/test/source/contacts/index.html +5 -0
  116. data/test/source/css/screen.css +76 -0
  117. data/test/source/deal.with.dots.html +7 -0
  118. data/test/source/foo/_posts/bar/2008-12-12-topical-post.textile +8 -0
  119. data/test/source/index.html +22 -0
  120. data/test/source/sitemap.xml +32 -0
  121. data/test/source/symlink-test/symlinked-file +22 -0
  122. data/test/source/win/_posts/2009-05-24-yaml-linebreak.markdown +7 -0
  123. data/test/source/z_category/_posts/2008-9-23-categories.textile +6 -0
  124. data/test/suite.rb +11 -0
  125. data/test/test_command.rb +39 -0
  126. data/test/test_configuration.rb +137 -0
  127. data/test/test_convertible.rb +51 -0
  128. data/test/test_core_ext.rb +88 -0
  129. data/test/test_filters.rb +102 -0
  130. data/test/test_generated_site.rb +83 -0
  131. data/test/test_json_driver.rb +63 -0
  132. data/test/test_kramdown.rb +35 -0
  133. data/test/test_new_command.rb +104 -0
  134. data/test/test_page.rb +193 -0
  135. data/test/test_pager.rb +115 -0
  136. data/test/test_post.rb +573 -0
  137. data/test/test_rdiscount.rb +22 -0
  138. data/test/test_redcarpet.rb +61 -0
  139. data/test/test_redcloth.rb +86 -0
  140. data/test/test_site.rb +374 -0
  141. data/test/test_tags.rb +310 -0
  142. data/test/test_yaml_driver.rb +35 -0
  143. metadata +554 -0
@@ -0,0 +1,455 @@
1
+ require 'set'
2
+
3
+ module Monad
4
+ class Site
5
+ attr_accessor :config, :layouts, :posts, :pages, :static_files,
6
+ :categories, :exclude, :include, :source, :dest, :lsi, :pygments,
7
+ :permalink_style, :tags, :time, :future, :safe, :plugins, :limit_posts,
8
+ :show_drafts, :keep_files, :baseurl, :data_sources
9
+
10
+ attr_accessor :converters, :generators
11
+
12
+ # Public: Initialize a new Site.
13
+ #
14
+ # config - A Hash containing site configuration details.
15
+ def initialize(config)
16
+ self.config = config.clone
17
+
18
+ self.safe = config['safe']
19
+ self.source = File.expand_path(config['source'])
20
+ self.dest = File.expand_path(config['destination'])
21
+ self.plugins = plugins_path
22
+ self.lsi = config['lsi']
23
+ self.pygments = config['pygments']
24
+ self.baseurl = config['baseurl']
25
+ self.permalink_style = config['permalink'].to_sym
26
+ self.exclude = config['exclude']
27
+ self.include = config['include']
28
+ self.future = config['future']
29
+ self.show_drafts = config['show_drafts']
30
+ self.limit_posts = config['limit_posts']
31
+ self.keep_files = config['keep_files']
32
+
33
+ self.reset
34
+ self.setup
35
+ end
36
+
37
+ # Public: Read, process, and write this Site to output.
38
+ #
39
+ # Returns nothing.
40
+ def process
41
+ self.reset
42
+ self.read
43
+ self.generate
44
+ self.render
45
+ self.cleanup
46
+ self.write
47
+ end
48
+
49
+ # Reset Site details.
50
+ #
51
+ # Returns nothing
52
+ def reset
53
+ self.time = if self.config['time']
54
+ Time.parse(self.config['time'].to_s)
55
+ else
56
+ Time.now
57
+ end
58
+ self.layouts = {}
59
+ self.posts = []
60
+ self.pages = []
61
+ self.static_files = []
62
+ self.categories = Hash.new { |hash, key| hash[key] = [] }
63
+ self.tags = Hash.new { |hash, key| hash[key] = [] }
64
+ self.data_sources = {}
65
+
66
+ if self.limit_posts < 0
67
+ raise ArgumentError, "limit_posts must be a non-negative number"
68
+ end
69
+ end
70
+
71
+ # Load necessary libraries, plugins, converters, and generators.
72
+ #
73
+ # Returns nothing.
74
+ def setup
75
+ require 'classifier' if self.lsi
76
+
77
+ # Check that the destination dir isn't the source dir or a directory
78
+ # parent to the source dir.
79
+ if self.source =~ /^#{self.dest}/
80
+ raise FatalException.new "Destination directory cannot be or contain the Source directory."
81
+ end
82
+
83
+ # If safe mode is off, load in any Ruby files under the plugins
84
+ # directory.
85
+ unless self.safe
86
+ self.plugins.each do |plugins|
87
+ Dir[File.join(plugins, "**/*.rb")].each do |f|
88
+ require f
89
+ end
90
+ end
91
+ end
92
+
93
+ self.converters = instantiate_subclasses(Monad::Converter)
94
+ self.generators = instantiate_subclasses(Monad::Generator)
95
+ end
96
+
97
+ # Internal: Setup the plugin search path
98
+ #
99
+ # Returns an Array of plugin search paths
100
+ def plugins_path
101
+ if (config['plugins'] == Monad::Configuration::DEFAULTS['plugins'])
102
+ [File.join(self.source, config['plugins'])]
103
+ else
104
+ Array(config['plugins']).map { |d| File.expand_path(d) }
105
+ end
106
+ end
107
+
108
+ # Read Site data from disk and load it into internal data structures.
109
+ #
110
+ # Returns nothing.
111
+ def read
112
+ self.read_layouts
113
+ self.read_directories
114
+ self.load_data_sources
115
+ end
116
+
117
+ # Read all the files in <source>/<layouts> and create a new Layout object
118
+ # with each one.
119
+ #
120
+ # Returns nothing.
121
+ def read_layouts
122
+ base = File.join(self.source, self.config['layouts'])
123
+ return unless File.exists?(base)
124
+ entries = []
125
+ Dir.chdir(base) { entries = filter_entries(Dir['*.*']) }
126
+
127
+ entries.each do |f|
128
+ name = f.split(".")[0..-2].join(".")
129
+ self.layouts[name] = Layout.new(self, base, f)
130
+ end
131
+ end
132
+
133
+ # Recursively traverse directories to find posts, pages and static files
134
+ # that will become part of the site according to the rules in
135
+ # filter_entries.
136
+ #
137
+ # dir - The String relative path of the directory to read. Default: ''.
138
+ #
139
+ # Returns nothing.
140
+ def read_directories(dir = '')
141
+ base = File.join(self.source, dir)
142
+ entries = Dir.chdir(base) { filter_entries(Dir.entries('.')) }
143
+
144
+ self.read_posts(dir)
145
+
146
+ if self.show_drafts
147
+ self.read_drafts(dir)
148
+ end
149
+
150
+ self.posts.sort!
151
+
152
+ # limit the posts if :limit_posts option is set
153
+ if limit_posts > 0
154
+ limit = self.posts.length < limit_posts ? self.posts.length : limit_posts
155
+ self.posts = self.posts[-limit, limit]
156
+ end
157
+
158
+ entries.each do |f|
159
+ f_abs = File.join(base, f)
160
+ f_rel = File.join(dir, f)
161
+ if File.directory?(f_abs)
162
+ next if self.dest.sub(/\/$/, '') == f_abs
163
+ read_directories(f_rel)
164
+ else
165
+ first3 = File.open(f_abs) { |fd| fd.read(3) }
166
+ if first3 == "---"
167
+ # file appears to have a YAML header so process it as a page
168
+ pages << Page.new(self, self.source, dir, f)
169
+ else
170
+ # otherwise treat it as a static file
171
+ static_files << StaticFile.new(self, self.source, dir, f)
172
+ end
173
+ end
174
+ end
175
+ end
176
+
177
+ # Read all the files in <source>/<dir>/_posts and create a new Post
178
+ # object with each one.
179
+ #
180
+ # dir - The String relative path of the directory to read.
181
+ #
182
+ # Returns nothing.
183
+ def read_posts(dir)
184
+ entries = get_entries(dir, '_posts')
185
+
186
+ # first pass processes, but does not yet render post content
187
+ entries.each do |f|
188
+ if Post.valid?(f)
189
+ post = Post.new(self, self.source, dir, f)
190
+
191
+ if post.published && (self.future || post.date <= self.time)
192
+ aggregate_post_info(post)
193
+ end
194
+ end
195
+ end
196
+ end
197
+
198
+ # Read all the files in <source>/<dir>/_drafts and create a new Post
199
+ # object with each one.
200
+ #
201
+ # dir - The String relative path of the directory to read.
202
+ #
203
+ # Returns nothing.
204
+ def read_drafts(dir)
205
+ entries = get_entries(dir, '_drafts')
206
+
207
+ # first pass processes, but does not yet render draft content
208
+ entries.each do |f|
209
+ if Draft.valid?(f)
210
+ draft = Draft.new(self, self.source, dir, f)
211
+
212
+ aggregate_post_info(draft)
213
+ end
214
+ end
215
+ end
216
+
217
+ # Run each of the Generators.
218
+ #
219
+ # Returns nothing.
220
+ def generate
221
+ self.generators.each do |generator|
222
+ generator.generate(self)
223
+ end
224
+ end
225
+
226
+ # Render the site to the destination.
227
+ #
228
+ # Returns nothing.
229
+ def render
230
+ payload = site_payload
231
+ self.posts.each do |post|
232
+ post.render(self.layouts, payload)
233
+ end
234
+
235
+ self.pages.each do |page|
236
+ relative_permalinks_deprecation_method if page.uses_relative_permalinks
237
+ page.render(self.layouts, payload)
238
+ end
239
+
240
+ self.categories.values.map { |ps| ps.sort! { |a, b| b <=> a } }
241
+ self.tags.values.map { |ps| ps.sort! { |a, b| b <=> a } }
242
+ rescue Errno::ENOENT => e
243
+ # ignore missing layout dir
244
+ end
245
+
246
+ # Remove orphaned files and empty directories in destination.
247
+ #
248
+ # Returns nothing.
249
+ def cleanup
250
+ # all files and directories in destination, including hidden ones
251
+ dest_files = Set.new
252
+ Dir.glob(File.join(self.dest, "**", "*"), File::FNM_DOTMATCH) do |file|
253
+ if self.keep_files.length > 0
254
+ dest_files << file unless file =~ /\/\.{1,2}$/ || file =~ keep_file_regex
255
+ else
256
+ dest_files << file unless file =~ /\/\.{1,2}$/
257
+ end
258
+ end
259
+
260
+ # files to be written
261
+ files = Set.new
262
+ self.posts.each do |post|
263
+ files << post.destination(self.dest)
264
+ end
265
+ self.pages.each do |page|
266
+ files << page.destination(self.dest)
267
+ end
268
+ self.static_files.each do |sf|
269
+ files << sf.destination(self.dest)
270
+ end
271
+
272
+ # adding files' parent directories
273
+ dirs = Set.new
274
+ files.each { |file| dirs << File.dirname(file) }
275
+ files.merge(dirs)
276
+
277
+ obsolete_files = dest_files - files
278
+ FileUtils.rm_rf(obsolete_files.to_a)
279
+ end
280
+
281
+ # Private: creates a regular expression from the keep_files array
282
+ #
283
+ # Examples
284
+ # ['.git','.svn'] creates the following regex: /\/(\.git|\/.svn)/
285
+ #
286
+ # Returns the regular expression
287
+ def keep_file_regex
288
+ or_list = self.keep_files.join("|")
289
+ pattern = "\/(#{or_list.gsub(".", "\.")})"
290
+ Regexp.new pattern
291
+ end
292
+
293
+ # Write static files, pages, and posts.
294
+ #
295
+ # Returns nothing.
296
+ def write
297
+ self.posts.each do |post|
298
+ post.write(self.dest)
299
+ end
300
+ self.pages.each do |page|
301
+ page.write(self.dest)
302
+ end
303
+ self.static_files.each do |sf|
304
+ sf.write(self.dest)
305
+ end
306
+ end
307
+
308
+ # Construct a Hash of Posts indexed by the specified Post attribute.
309
+ #
310
+ # post_attr - The String name of the Post attribute.
311
+ #
312
+ # Examples
313
+ #
314
+ # post_attr_hash('categories')
315
+ # # => { 'tech' => [<Post A>, <Post B>],
316
+ # # 'ruby' => [<Post B>] }
317
+ #
318
+ # Returns the Hash: { attr => posts } where
319
+ # attr - One of the values for the requested attribute.
320
+ # posts - The Array of Posts with the given attr value.
321
+ def post_attr_hash(post_attr)
322
+ # Build a hash map based on the specified post attribute ( post attr =>
323
+ # array of posts ) then sort each array in reverse order.
324
+ hash = Hash.new { |hsh, key| hsh[key] = Array.new }
325
+ self.posts.each { |p| p.send(post_attr.to_sym).each { |t| hash[t] << p } }
326
+ hash.values.map { |sortme| sortme.sort! { |a, b| b <=> a } }
327
+ hash
328
+ end
329
+
330
+ # The Hash payload containing site-wide data.
331
+ #
332
+ # Returns the Hash: { "site" => data } where data is a Hash with keys:
333
+ # "time" - The Time as specified in the configuration or the
334
+ # current time if none was specified.
335
+ # "posts" - The Array of Posts, sorted chronologically by post date
336
+ # and then title.
337
+ # "pages" - The Array of all Pages.
338
+ # "html_pages" - The Array of HTML Pages.
339
+ # "categories" - The Hash of category values and Posts.
340
+ # See Site#post_attr_hash for type info.
341
+ # "tags" - The Hash of tag values and Posts.
342
+ # See Site#post_attr_hash for type info.
343
+ def site_payload
344
+ {"site" => self.data_sources.merge(self.config).merge({
345
+ "time" => self.time,
346
+ "posts" => self.posts.sort { |a, b| b <=> a },
347
+ "pages" => self.pages,
348
+ "html_pages" => self.pages.reject { |page| !page.html? },
349
+ "categories" => post_attr_hash('categories'),
350
+ "tags" => post_attr_hash('tags')})}
351
+ end
352
+
353
+ # Filter out any files/directories that are hidden or backup files (start
354
+ # with "." or "#" or end with "~"), or contain site content (start with "_"),
355
+ # or are excluded in the site configuration, unless they are web server
356
+ # files such as '.htaccess'.
357
+ #
358
+ # entries - The Array of String file/directory entries to filter.
359
+ #
360
+ # Returns the Array of filtered entries.
361
+ def filter_entries(entries)
362
+ entries.reject do |e|
363
+ unless self.include.glob_include?(e)
364
+ ['.', '_', '#'].include?(e[0..0]) ||
365
+ e[-1..-1] == '~' ||
366
+ self.exclude.glob_include?(e) ||
367
+ (File.symlink?(e) && self.safe)
368
+ end
369
+ end
370
+ end
371
+
372
+ # Get the implementation class for the given Converter.
373
+ #
374
+ # klass - The Class of the Converter to fetch.
375
+ #
376
+ # Returns the Converter instance implementing the given Converter.
377
+ def getConverterImpl(klass)
378
+ matches = self.converters.select { |c| c.class == klass }
379
+ if impl = matches.first
380
+ impl
381
+ else
382
+ raise "Converter implementation not found for #{klass}"
383
+ end
384
+ end
385
+
386
+ # Create array of instances of the subclasses of the class or module
387
+ # passed in as argument.
388
+ #
389
+ # klass - class or module containing the subclasses which should be
390
+ # instantiated
391
+ #
392
+ # Returns array of instances of subclasses of parameter
393
+ def instantiate_subclasses(klass)
394
+ klass.subclasses.select do |c|
395
+ !self.safe || c.safe
396
+ end.sort.map do |c|
397
+ c.new(self.config)
398
+ end
399
+ end
400
+
401
+ # Read the entries from a particular directory for processing
402
+ #
403
+ # dir - The String relative path of the directory to read
404
+ # subfolder - The String directory to read
405
+ #
406
+ # Returns the list of entries to process
407
+ def get_entries(dir, subfolder)
408
+ base = File.join(self.source, dir, subfolder)
409
+ return [] unless File.exists?(base)
410
+ entries = Dir.chdir(base) { filter_entries(Dir['**/*']) }
411
+ entries.delete_if { |e| File.directory?(File.join(base, e)) }
412
+ end
413
+
414
+ # Aggregate post information
415
+ #
416
+ # post - The Post object to aggregate information for
417
+ #
418
+ # Returns nothing
419
+ def aggregate_post_info(post)
420
+ self.posts << post
421
+ post.categories.each { |c| self.categories[c] << post }
422
+ post.tags.each { |c| self.tags[c] << post }
423
+ end
424
+
425
+ def relative_permalinks_deprecation_method
426
+ if config['relative_permalinks'] && !@deprecated_relative_permalinks
427
+ $stderr.puts # Places newline after "Generating..."
428
+ Monad::Logger.warn "Deprecation:", "Starting in 1.1, permalinks for pages" +
429
+ " in subfolders must be relative to the" +
430
+ " site source directory, not the parent" +
431
+ " directory. Check http://monadrb.com/docs/upgrading/"+
432
+ " for more info."
433
+ $stderr.print Monad::Logger.formatted_topic("") + "..." # for "done."
434
+ @deprecated_relative_permalinks = true
435
+ end
436
+ end
437
+
438
+ # Load external data sources to @data_sources
439
+ #
440
+ # Returns nothing
441
+ def load_data_sources
442
+ self.config['data_sources'] && self.config['data_sources'].each do |data_source_config|
443
+ if data_source_config['name'] !~ /^\w+$/
444
+ raise FatalException.new "Bad data source name: #{data_source_config['name']}. Only letters or digits allowed in data source name."
445
+ end
446
+
447
+ # create driver
448
+ driver_name = data_source_config['type'].split('_').collect!{ |w| w.capitalize }.join + 'Driver'
449
+ driver = Monad::Drivers.const_get(driver_name).new(data_source_config)
450
+
451
+ @data_sources[data_source_config['name']] = driver.load
452
+ end
453
+ end
454
+ end
455
+ end