monad 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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