PageRenderTime 38ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 0ms

/Languages/Ruby/Samples/Tutorial/app/tutorial.rb

https://github.com/thomo13/ironruby
Ruby | 499 lines | 387 code | 79 blank | 33 comment | 36 complexity | 441cb285981205d808efb766d5c5017b MD5 | raw file
  1. # ****************************************************************************
  2. #
  3. # Copyright (c) Microsoft Corporation.
  4. #
  5. # This source code is subject to terms and conditions of the Apache License, Version 2.0. A
  6. # copy of the license can be found in the License.html file at the root of this distribution. If
  7. # you cannot locate the Apache License, Version 2.0, please send an email to
  8. # ironruby@microsoft.com. By using this source code in any fashion, you are agreeing to be bound
  9. # by the terms of the Apache License, Version 2.0.
  10. #
  11. # You must not remove this notice, or any other, from this software.
  12. #
  13. #
  14. # ****************************************************************************
  15. SILVERLIGHT = false unless defined? SILVERLIGHT
  16. require "stringio"
  17. module Tutorial
  18. class Summary
  19. attr :title
  20. attr :description
  21. def initialize(title, desc)
  22. @title = title
  23. @description = desc
  24. end
  25. def body
  26. @description
  27. end
  28. def to_s
  29. @title
  30. end
  31. end
  32. class Task
  33. attr :description
  34. attr :run_unless
  35. attr :setup
  36. attr :code
  37. attr :title
  38. attr :source_files # Files used by the task. The user might want to browse these files
  39. attr :test_hook
  40. def initialize title, description, run_unless, setup, code, source_files, test_hook, &success_evaluator
  41. @title = title
  42. @description = description
  43. @run_unless = run_unless
  44. @setup = setup
  45. @code = code
  46. @source_files = source_files
  47. @test_hook = test_hook
  48. @success_evaluator = success_evaluator
  49. end
  50. def should_run?(bind)
  51. if not @run_unless
  52. return true
  53. end
  54. begin
  55. return (not @run_unless.call(bind))
  56. rescue
  57. return true
  58. end
  59. end
  60. def success?(interaction, show_errors=false)
  61. begin
  62. if @success_evaluator
  63. return (@success_evaluator.call(interaction) ? true : false)
  64. else
  65. old_verbose, $VERBOSE = $VERBOSE, nil
  66. begin
  67. return eval(code_string, interaction.bind) == interaction.result
  68. ensure
  69. $VERBOSE = old_verbose
  70. end
  71. end
  72. rescue => e
  73. warn %{success_evaluator raised error: #{e}} if show_errors
  74. return false
  75. end
  76. end
  77. def code_string
  78. c = code
  79. c = c.to_ary.join("\n") if c.respond_to? :to_ary
  80. c
  81. end
  82. end
  83. class Chapter
  84. attr :name, true # TODO - true is needed only to workaround data-binding bug
  85. attr :introduction, true
  86. attr :summary, true
  87. attr :tasks, true
  88. attr :next_item, true
  89. def initialize name, introduction = nil, summary = nil, tasks = [], next_item = nil
  90. @name = name
  91. @introduction = introduction
  92. @summary = summary
  93. @tasks = tasks
  94. @next_item = next_item
  95. end
  96. def to_s
  97. @name
  98. end
  99. end
  100. class Section
  101. attr :name, true # TODO - true is needed only to workaround data-binding bug
  102. attr :introduction, true
  103. attr :chapters, true
  104. def initialize name, introduction = nil, chapters = []
  105. @name = name
  106. @introduction = introduction
  107. @chapters = chapters
  108. end
  109. def to_s
  110. @name
  111. end
  112. def first_chapter
  113. @chapters.first rescue nil
  114. end
  115. end
  116. class Tutorial
  117. attr :name
  118. attr :file
  119. attr :introduction, true
  120. attr :legal_notice, true
  121. attr :summary, true
  122. attr :sections, true
  123. def initialize name, file, introduction = nil, notice = nil, summary = nil, sections = []
  124. @name = name
  125. @file = file
  126. @introduction = introduction
  127. @legal_notice = notice
  128. @summary = summary
  129. @sections = sections
  130. end
  131. def to_s
  132. @name
  133. end
  134. def first_chapter
  135. first_section.first_chapter rescue nil
  136. end
  137. def first_section
  138. @sections.first rescue nil
  139. end
  140. end
  141. @@tutorials = {} unless class_variable_defined? :@@tutorials
  142. def self.add_tutorial(tutorial)
  143. # Chain the chapters to the next section or chapter
  144. prev_chapter = nil
  145. tutorial.sections.each do |section|
  146. if prev_chapter
  147. prev_chapter.next_item = section
  148. prev_chapter = nil
  149. end
  150. section.chapters.each do |chapter|
  151. if prev_chapter
  152. prev_chapter.next_item = chapter
  153. end
  154. prev_chapter = chapter
  155. end
  156. end
  157. @@tutorials[tutorial.file] = tutorial
  158. end
  159. def self.all
  160. all_files = [
  161. 'Tutorials/ironruby_tutorial.rb',
  162. 'Tutorials/tryruby_tutorial.rb',
  163. 'Tutorials/hosting_tutorial.rb']
  164. if SILVERLIGHT
  165. all_files.each {|f| get_tutorial f }
  166. else
  167. Dir[File.expand_path("Tutorials/*_tutorial.rb", File.dirname(__FILE__))].each do |t|
  168. self.get_tutorial t unless File.directory?(t)
  169. end
  170. abort("List of files need to be updated for Silverlight") unless @@tutorials.size == all_files.size or not ENV['DLR_ROOT']
  171. end
  172. @@tutorials
  173. end
  174. def self.get_tutorial path = nil
  175. if not path
  176. all
  177. return @@tutorials.values.first
  178. end
  179. if SILVERLIGHT
  180. # TODO - On Silverlight, currently __FILE__ does not include the folders, and File.expand_path does not
  181. # work either. As a workaround, we drop all folder names
  182. path_key = File.basename(path)
  183. else
  184. path = File.expand_path path, File.dirname(__FILE__)
  185. path_key = path
  186. end
  187. if not @@tutorials.has_key? path_key
  188. require path
  189. raise "#{path} does not contains a tutorial definition" if not @@tutorials.has_key? path_key
  190. end
  191. return @@tutorials[path_key]
  192. end
  193. class ReplContext
  194. attr :scope
  195. attr :bind
  196. def initialize
  197. @partial_input = ""
  198. @scope = Object.new
  199. class << @scope
  200. def include(*a)
  201. self.class.instance_eval { include(*a) }
  202. end
  203. def to_s
  204. "main (tutorial)"
  205. end
  206. end
  207. @bind = @scope.instance_eval { binding }
  208. # Allow the tutorial DSL to use i.bind.foo in the success-evaluator blocks
  209. def @bind.method_missing name, *args
  210. if args.empty?
  211. eval name.to_s, self
  212. else
  213. m = eval "method #{name.inspect}", self
  214. m.call *args
  215. end
  216. end
  217. end
  218. def interact input
  219. # Redirect stdout. Note that this affects the entire process. If the program calls "puts"
  220. # for some reason on another thread, the user may not expect to see the output. But it is
  221. # hard to distinguish between printing that the user initiated, and printing that the program
  222. # itself is doing.
  223. output = StringIO.new
  224. old_stdout, $stdout = $stdout, output
  225. old_verbose, $VERBOSE = $VERBOSE, nil
  226. result = nil
  227. error = nil
  228. full_input = @partial_input + input.to_s
  229. Thread.current[:evaluating_tutorial_input] = true
  230. begin
  231. result = eval(full_input, @bind) # TODO - to_s should not be needed here
  232. rescue Exception => error
  233. raise error if error.kind_of? SystemExit
  234. ensure
  235. $stdout = old_stdout
  236. Thread.current[:evaluating_tutorial_input] = nil
  237. $VERBOSE = old_verbose
  238. end
  239. if error.kind_of? SyntaxError and error.message =~ /unexpected (\$end|end of file)/
  240. @partial_input += input
  241. @partial_input += "\n" unless input[input.size-1] == "\n" # TODO - input[-1] seems to cause IndexError
  242. InteractionResult.new(@bind, "", "", nil, nil, true)
  243. else
  244. @partial_input = ""
  245. InteractionResult.new(@bind, full_input, output.string, result, error)
  246. end
  247. end
  248. def reset_input
  249. @partial_input = ""
  250. end
  251. end
  252. class InteractionResult
  253. attr :bind
  254. # This should be used very sparingly. Since it checks directly what the user typed, it can have too many
  255. # false positives and negatives.
  256. # TODO - Remove this when there is good mocking/stubbing support
  257. attr :input
  258. attr :output
  259. attr :result
  260. attr :error
  261. def initialize(bind, input, output, result, error = nil, partial_input = false)
  262. @bind = bind
  263. @input = input
  264. @output = output
  265. @result = result
  266. @error = error
  267. @partial_input = partial_input
  268. raise "result should be nil if an exception was raised" if result and error
  269. end
  270. def to_s
  271. %{InteractionResult output=#{@output}, result=#{@error ? "(error)" : @result.inspect}, error=#{@error ? @error : "(none)"}}
  272. end
  273. def result
  274. raise "Interaction resulted in an exception" if error or @partial_input
  275. @result
  276. end
  277. def error
  278. raise "Partial input received" if @partial_input
  279. @error
  280. end
  281. def partial_input?
  282. @partial_input
  283. end
  284. end
  285. # Simple stub class for mocking.
  286. # TODO - move to a real mocking framework
  287. class Stub
  288. def initialize() @calls = [] end
  289. def respond_to?(name) true end
  290. def method_missing name, *args
  291. @calls << name
  292. Stub.new
  293. end
  294. def called?(name) @calls.include? name end
  295. end
  296. def self.stub() Stub.new end
  297. # Utility method to verify that a handler was added to an event
  298. # Since there is no easily visible side-effect to adding a handler, we monkey-patch the event
  299. # with a method that will set a flag attribute in a given module
  300. def self.snoop_add_handler tutorial_module, event_name, obj
  301. flag_name = event_name + "_flag"
  302. klass = class << tutorial_module
  303. self
  304. end
  305. klass.module_eval do
  306. attr_accessor(flag_name.to_sym)
  307. end
  308. tutorial_module.instance_variable_set(("@" + flag_name).to_sym, false)
  309. before_tutorial_name = "before_tutorial_" + event_name
  310. klass = class << obj
  311. self
  312. end
  313. if not klass.method_defined?(before_tutorial_name.to_sym)
  314. klass.instance_eval { alias_method(before_tutorial_name.to_sym, event_name.to_sym) }
  315. klass.class_eval %{
  316. def #{event_name} *a, &b
  317. if block_given?
  318. #{before_tutorial_name} *a, &b
  319. #{tutorial_module}.#{flag_name} = true
  320. else
  321. #{before_tutorial_name} *a
  322. end
  323. end
  324. }
  325. end
  326. end
  327. end
  328. class Object
  329. def tutorial name
  330. raise "Only one tutorial can be under creation at a time" if Thread.current[:tutorial]
  331. caller[0] =~ /\A(.*):[0-9]+/
  332. tutorial_file = $1
  333. tutorial_file = File.basename(tutorial_file) if SILVERLIGHT # __FILE__ may not be the full required path
  334. t = Tutorial::Tutorial.new name, tutorial_file
  335. Thread.current[:tutorial] = t
  336. yield
  337. Tutorial.add_tutorial t
  338. Thread.current[:tutorial] = nil
  339. end
  340. def introduction intro
  341. if Thread.current[:chapter]
  342. Thread.current[:chapter].introduction = intro
  343. elsif Thread.current[:section]
  344. Thread.current[:section].introduction = intro
  345. elsif Thread.current[:tutorial]
  346. Thread.current[:tutorial].introduction = intro
  347. else
  348. raise "introduction should only be used within a tutorial definition"
  349. end
  350. end
  351. def legal notice
  352. raise "legal should only be used within a tutorial definition" unless Thread.current[:tutorial]
  353. Thread.current[:tutorial].legal_notice = notice
  354. end
  355. def summary s
  356. s = if s.kind_of?(String)
  357. Tutorial::Summary.new nil, s
  358. else
  359. opts = {:title => "Section complete!"}.merge(s)
  360. Tutorial::Summary.new opts[:title], opts[:body]
  361. end
  362. if Thread.current[:chapter]
  363. Thread.current[:chapter].summary = s
  364. elsif Thread.current[:tutorial]
  365. Thread.current[:tutorial].summary = s
  366. else
  367. raise "summary should only be used within a tutorial or chapter definition"
  368. end
  369. end
  370. def section name
  371. raise "Only one section can be under creation at a time" if Thread.current[:section]
  372. section = Tutorial::Section.new name
  373. Thread.current[:section] = section
  374. Thread.current[:platform_match] = nil
  375. yield
  376. if Thread.current[:platform_match] == nil or Thread.current[:platform_match]
  377. Thread.current[:tutorial].sections << section
  378. end
  379. Thread.current[:section] = nil
  380. end
  381. def silverlight(enabled = true)
  382. if not Thread.current[:section]
  383. raise "platform should only be used within a section definition"
  384. end
  385. Thread.current[:"platform_match#{Thread.current[:chapter] ? "_chapter" : nil}"] = (SILVERLIGHT == enabled)
  386. end
  387. def chapter name
  388. raise "Only one chapter can be under creation at a time" if Thread.current[:chapter]
  389. chapter = Tutorial::Chapter.new name
  390. Thread.current[:chapter] = chapter
  391. Thread.current[:platform_match_chapter] = nil
  392. yield
  393. if Thread.current[:platform_match_chapter] == nil or Thread.current[:platform_match_chapter]
  394. Thread.current[:section].chapters << chapter
  395. end
  396. Thread.current[:chapter] = nil
  397. end
  398. def task(options, &success_evaluator)
  399. options = {}.merge(options)
  400. if options.has_key?(:silverlight) and (options[:silverlight] != SILVERLIGHT)
  401. return
  402. end
  403. Thread.current[:chapter].tasks << Tutorial::Task.new(
  404. options[:title],
  405. options[:body],
  406. options[:run_unless],
  407. options[:setup],
  408. options[:code],
  409. options[:source_files],
  410. options[:test_hook],
  411. &success_evaluator)
  412. end
  413. end
  414. class String
  415. def strip_margin
  416. /( *)\w/ =~ self
  417. match = $1
  418. gsub(/^#{match}/, "")
  419. end
  420. end