belajar 0.1.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 (91) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/.travis.yml +6 -0
  4. data/Gemfile +4 -0
  5. data/Guardfile +5 -0
  6. data/README.md +5 -0
  7. data/Rakefile +5 -0
  8. data/belajar.gemspec +38 -0
  9. data/bin/belajar +12 -0
  10. data/lib/belajar.rb +24 -0
  11. data/lib/belajar/chapter.rb +23 -0
  12. data/lib/belajar/configuration.rb +89 -0
  13. data/lib/belajar/congratulator.rb +13 -0
  14. data/lib/belajar/course.rb +80 -0
  15. data/lib/belajar/exceptions.rb +19 -0
  16. data/lib/belajar/generator.rb +60 -0
  17. data/lib/belajar/github_client.rb +30 -0
  18. data/lib/belajar/loadable.rb +23 -0
  19. data/lib/belajar/loading/chapters.rb +9 -0
  20. data/lib/belajar/loading/courses.rb +9 -0
  21. data/lib/belajar/loading/units.rb +9 -0
  22. data/lib/belajar/reference_solution.rb +18 -0
  23. data/lib/belajar/solution.rb +53 -0
  24. data/lib/belajar/storeable.rb +32 -0
  25. data/lib/belajar/task.rb +10 -0
  26. data/lib/belajar/terminal.rb +12 -0
  27. data/lib/belajar/terminal/cli.rb +59 -0
  28. data/lib/belajar/terminal/courses.rb +179 -0
  29. data/lib/belajar/terminal/output.rb +78 -0
  30. data/lib/belajar/terminal/setup.rb +115 -0
  31. data/lib/belajar/terminal/solutions.rb +46 -0
  32. data/lib/belajar/terminal/texts/about.txt +19 -0
  33. data/lib/belajar/terminal/texts/congratulations.txt +12 -0
  34. data/lib/belajar/terminal/texts/courses_empty.txt +3 -0
  35. data/lib/belajar/terminal/texts/hint_course_download.txt +13 -0
  36. data/lib/belajar/terminal/texts/welcome.txt +12 -0
  37. data/lib/belajar/terminal/welcome.rb +98 -0
  38. data/lib/belajar/test.rb +46 -0
  39. data/lib/belajar/test_result.rb +86 -0
  40. data/lib/belajar/unit.rb +28 -0
  41. data/lib/belajar/version.rb +3 -0
  42. data/lib/belajar/views.rb +51 -0
  43. data/lib/belajar/views/chapters_menu.rb +60 -0
  44. data/lib/belajar/views/courses_menu.rb +52 -0
  45. data/lib/belajar/views/main_menu.rb +37 -0
  46. data/lib/belajar/views/menu.rb +89 -0
  47. data/lib/belajar/views/splash.rb +57 -0
  48. data/lib/belajar/views/task_view.rb +236 -0
  49. data/lib/belajar/views/top_bar.rb +40 -0
  50. data/lib/belajar/views/units_menu.rb +61 -0
  51. data/lib/belajar/window.rb +215 -0
  52. data/spec/belajar/chapter_spec.rb +76 -0
  53. data/spec/belajar/configuration_spec.rb +161 -0
  54. data/spec/belajar/congratulator_spec.rb +24 -0
  55. data/spec/belajar/course_spec.rb +201 -0
  56. data/spec/belajar/generator_spec.rb +82 -0
  57. data/spec/belajar/github_client_spec.rb +53 -0
  58. data/spec/belajar/loading/chapters_spec.rb +16 -0
  59. data/spec/belajar/loading/courses_spec.rb +16 -0
  60. data/spec/belajar/loading/units_spec.rb +21 -0
  61. data/spec/belajar/reference_solution_spec.rb +41 -0
  62. data/spec/belajar/solution_spec.rb +86 -0
  63. data/spec/belajar/storeable_spec.rb +35 -0
  64. data/spec/belajar/task_spec.rb +23 -0
  65. data/spec/belajar/terminal/cli_spec.rb +51 -0
  66. data/spec/belajar/terminal/courses_spec.rb +293 -0
  67. data/spec/belajar/terminal/output_spec.rb +151 -0
  68. data/spec/belajar/terminal/setup_spec.rb +10 -0
  69. data/spec/belajar/terminal/solutions_spec.rb +8 -0
  70. data/spec/belajar/terminal/welcome_spec.rb +12 -0
  71. data/spec/belajar/terminal_spec.rb +24 -0
  72. data/spec/belajar/test_example_spec.rb +54 -0
  73. data/spec/belajar/test_result_spec.rb +91 -0
  74. data/spec/belajar/test_spec.rb +48 -0
  75. data/spec/belajar/unit_spec.rb +85 -0
  76. data/spec/belajar/views/chapters_menu_spec.rb +6 -0
  77. data/spec/belajar/views/courses_menu_spec.rb +6 -0
  78. data/spec/belajar/views/menu_spec.rb +19 -0
  79. data/spec/belajar/views/task_view_spec.rb +7 -0
  80. data/spec/belajar/views/units_menu_spec.rb +6 -0
  81. data/spec/belajar/views_spec.rb +21 -0
  82. data/spec/belajar_spec.rb +51 -0
  83. data/spec/path_helpers_spec.rb +60 -0
  84. data/spec/resource_helpers_spec.rb +33 -0
  85. data/spec/spec_helper.rb +28 -0
  86. data/spec/support/macros/content_helpers.rb +129 -0
  87. data/spec/support/macros/mock_helpers.rb +25 -0
  88. data/spec/support/macros/path_helpers.rb +139 -0
  89. data/spec/support/macros/resource_helpers.rb +157 -0
  90. data/spec/support/macros/test_helpers.rb +6 -0
  91. metadata +385 -0
@@ -0,0 +1,23 @@
1
+ module Belajar
2
+ module Loadable
3
+
4
+ require 'active_support/inflector'
5
+
6
+ def load(path)
7
+ if Dir.exist?(path)
8
+ dirs = Dir.entries(path).select do |entry|
9
+ !entry.match(/\./)
10
+ end
11
+
12
+ dirs.sort.map do |dir|
13
+ dir_path = File.join(path, dir)
14
+ class_name = self.to_s.demodulize.singularize
15
+ "Belajar::#{class_name}".constantize.new(dir_path)
16
+ end
17
+ else
18
+ Array.new
19
+ end
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,9 @@
1
+ module Belajar
2
+ module Loading
3
+
4
+ class Chapters
5
+ extend Belajar::Loadable
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Belajar
2
+ module Loading
3
+
4
+ class Courses
5
+ extend Belajar::Loadable
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Belajar
2
+ module Loading
3
+
4
+ class Units
5
+ extend Belajar::Loadable
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,18 @@
1
+ module Belajar
2
+ class ReferenceSolution
3
+ attr_reader :path
4
+
5
+ def initialize(path)
6
+ @path = Dir[File.join(path, '*solution.rb')].first
7
+ @code = File.read(@path).strip if @path
8
+ end
9
+
10
+ def code
11
+ @code.to_s
12
+ end
13
+
14
+ def code_lines
15
+ code.lines.map(&:chomp)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,53 @@
1
+ module Belajar
2
+ class Solution
3
+
4
+ FILE_SUFFIX = '_solution.rb'
5
+
6
+ attr_reader :code, :path, :errors
7
+
8
+ def initialize(unit_path)
9
+ @unit_path = unit_path
10
+ @path = solution_path(unit_path)
11
+ @code = File.read(@path).strip if File.file?(@path)
12
+ @verified = get_store_state
13
+ end
14
+
15
+ def verify!
16
+ result = Belajar::Test.new(@unit_path).run(self.code)
17
+ set_store_state(result.passed?)
18
+ result
19
+ end
20
+
21
+ def verified?
22
+ !!@verified
23
+ end
24
+
25
+ def store_key
26
+ unless @store_key
27
+ part_path = path.split('/')[-3..-1].join('/').gsub(FILE_SUFFIX, '')
28
+ @store_key = Storeable.key(part_path, prefix: 'verified')
29
+ end
30
+
31
+ @store_key
32
+ end
33
+
34
+ private
35
+
36
+ def solution_path(path)
37
+ local_path = Belajar.config.solutions_path
38
+ sub_dirs = Storeable.key(path.split('/')[-3..-2].join('/').gsub(FILE_SUFFIX, ''))
39
+ file = Storeable.key(File.basename(path)) + FILE_SUFFIX
40
+
41
+ File.join(local_path, sub_dirs, file)
42
+ end
43
+
44
+ def set_store_state(verified)
45
+ @verified = verified
46
+ QuickStore.store.set(store_key, verified?)
47
+ end
48
+
49
+ def get_store_state
50
+ QuickStore.store.get(store_key)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,32 @@
1
+ module Belajar
2
+ module Storeable
3
+
4
+ LEADING_NUMBERS = /^\d+[\_\-\s]+/
5
+ PART_JOINTS = /[\_\-\s]+/
6
+
7
+ class << self
8
+ def key(text, options = {})
9
+ separator = QuickStore.config.key_separator
10
+ prefix = options[:prefix]
11
+ suffix = clean(options[:suffix])
12
+ suffixes = options[:suffixes]
13
+ suffixes_items = suffixes ? suffixes.map { |s| clean(s) }.compact : nil
14
+
15
+ [prefix, clean(text), suffix || suffixes_items].compact.join(separator)
16
+ end
17
+
18
+ private
19
+
20
+ def clean(text)
21
+ if text
22
+ parts = text.to_s.split(QuickStore.config.key_separator).map do |key|
23
+ key.gsub(LEADING_NUMBERS, '').gsub(PART_JOINTS, '_').downcase
24
+ end
25
+
26
+ parts.join(QuickStore.config.key_separator)
27
+ end
28
+ end
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,10 @@
1
+ module Belajar
2
+ class Task
3
+ attr_reader :markdown, :path
4
+
5
+ def initialize(path)
6
+ @path = Dir[File.join(path, '*.md')].first
7
+ @markdown = File.read(@path).strip
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,12 @@
1
+ module Belajar
2
+ module Terminal
3
+
4
+ # text should be of a width of 70 columns or less
5
+ def self.text(file_name)
6
+ texts_path = File.expand_path('../terminal/texts', __FILE__)
7
+ file = File.join(texts_path, "#{file_name.to_s}.txt")
8
+ (File.exist?(file) ? File.read(file).to_s : '')
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,59 @@
1
+ require 'thor'
2
+
3
+ module Belajar
4
+ module Terminal
5
+
6
+ require_relative 'courses'
7
+ require_relative 'solutions'
8
+ require_relative 'setup'
9
+ require_relative 'output'
10
+
11
+ class CLI < Thor
12
+ include Terminal::Output
13
+
14
+ desc 'courses [COMMAND]', 'Handle belajar courses'
15
+ subcommand 'courses', Terminal::Courses
16
+
17
+ desc 'solutions [COMMAND]', 'Handle your solutions'
18
+ subcommand 'solutions', Terminal::Solutions
19
+
20
+ desc 'setup [COMMAND]', 'Change belajar setup'
21
+ subcommand 'setup', Terminal::Setup
22
+
23
+ def self.start
24
+ Belajar.config.import!
25
+ super
26
+ end
27
+
28
+ desc 'about', 'About belajar'
29
+ def about
30
+ Welcome.about
31
+ end
32
+
33
+ desc 'welcome', 'Setup belajar the first time and learn some important commands.'
34
+ def welcome
35
+ Welcome.run
36
+ end
37
+
38
+ desc 'scaffold', 'Scaffold solution files for your courses.'
39
+ def scaffold
40
+ generator = Generator.new
41
+ generator.prepare
42
+
43
+ courses_path = Belajar.config.courses_path
44
+ solutions_path = Belajar.config.solutions_path
45
+
46
+ generator.scaffold(courses_path, solutions_path)
47
+
48
+ say_info "You will find your solution files in\n#{solutions_path}."
49
+ end
50
+
51
+ desc 'learn', 'Go to belajar to learn Ruby!'
52
+ def learn
53
+ courses = Loading::Courses.load(Belajar.config.courses_path)
54
+ courses.empty? ? Courses.new.list : Belajar.start
55
+ end
56
+ end
57
+
58
+ end
59
+ end
@@ -0,0 +1,179 @@
1
+ module Belajar
2
+ module Terminal
3
+
4
+ require 'os'
5
+ require 'open-uri'
6
+ require 'zip'
7
+ require_relative 'output'
8
+
9
+ class Courses < Thor
10
+ include Terminal::Output
11
+
12
+ desc 'list', 'List your available belajar courses'
13
+ def list
14
+ courses = Loading::Courses.load(Belajar.config.courses_path)
15
+ say_info courses_list_text(courses)
16
+ end
17
+
18
+ method_option :github, type: :string, aliases: '-g', desc: 'Download Github repository'
19
+ desc 'download [URL] [OPTIONS]', 'Download a new belajar course from [URL]'
20
+ def download(url = nil, action = 'downloaded')
21
+ use_initial_course = url.nil? && options[:github].nil?
22
+ url = GithubClient.master_zip_url(Belajar.config.initial_course) if use_initial_course
23
+ url = GithubClient.master_zip_url(options[:github]) if options[:github]
24
+
25
+ url_given = (url =~ /\A#{URI::regexp(['http', 'https'])}\z/)
26
+ github = use_initial_course || options[:github] || url.match(/github\.com/)
27
+
28
+ raise Download::NoUrlError unless url_given
29
+ raise Download::NoZipFileUrlError unless File.basename(url) =~ /\.zip/
30
+
31
+ courses_path = Belajar.config.courses_path
32
+ FileUtils.makedirs(courses_path) unless Dir.exist?(courses_path)
33
+
34
+ file_name = File.join(courses_path, url.split('/').last)
35
+
36
+ File.open(file_name, 'w') { |file| file << open(url).read }
37
+ course = Course.unzip(file_name, github_repo: github)
38
+
39
+ if github
40
+ user_and_repo = url.match(/github.com\/(.*)\/archive\/master.zip/).captures.first
41
+ store_repo_data(options[:github] || user_and_repo)
42
+ end
43
+
44
+ QuickStore.store.set(course.key(:url), url)
45
+ QuickStore.store.set(course.key(:updated_at), Time.now.to_s)
46
+ scaffold_solutions
47
+
48
+ say_info "Successfully #{action} the course \"#{course.title}\"!"
49
+ rescue Download::NoUrlError => e
50
+ print_download_warning(url, "\"#{url}\" is not a valid URL!")
51
+ rescue Download::NoZipFileUrlError => e
52
+ print_download_warning(url, "\"#{url}\" is not a URL of a *.zip file!")
53
+ rescue Exception => e
54
+ print_download_warning(url, e.message)
55
+ ensure
56
+ FileUtils.rm(file_name) if File.exist?(file_name.to_s)
57
+ end
58
+
59
+ method_option :all, type: :boolean, aliases: '-a', desc: 'Update all courses'
60
+ desc 'update [COURSE_NAME] [OPTIONS]', 'Update Daigak courses.'
61
+ def update(course_name = nil)
62
+ if options[:all]
63
+ courses = Loading::Courses.load(Belajar.config.courses_path)
64
+ courses.each { |course| update_course(course) }
65
+ elsif course_name
66
+ path = File.join(Belajar.config.courses_path, course_name)
67
+
68
+ unless Dir.exist?(path)
69
+ print_course_not_available(course_name)
70
+ return
71
+ end
72
+
73
+ update_course(Course.new(course_name))
74
+ else
75
+ system 'belajar course help update'
76
+ end
77
+ end
78
+
79
+ method_option :all, type: :boolean, aliases: '-a', desc: 'Delete all courses'
80
+ desc 'delete [COURSE_NAME] [OPTIONS]', 'Delete Belajar courses.'
81
+ def delete(course_name = nil)
82
+ if options[:all]
83
+ get_confirm('Are you shure you want to delete all courses?') do
84
+ course_dirs = Dir[File.join(Belajar.config.courses_path, '*')]
85
+
86
+ course_dirs.each do |dir|
87
+ FileUtils.remove_dir(dir)
88
+ QuickStore.store.delete(Storeable.key(File.basename(dir), prefix: 'courses'))
89
+ end
90
+
91
+ say_info "All belajar courses were successfully deleted."
92
+ end
93
+ elsif course_name
94
+ path = File.join(Belajar.config.courses_path, course_name)
95
+
96
+ unless Dir.exist?(path)
97
+ print_course_not_available(course_name)
98
+ return
99
+ end
100
+
101
+ get_confirm("Are you shure you want to delete the course \"#{course_name}\"?") do
102
+ FileUtils.remove_dir(path)
103
+ QuickStore.store.delete(Storeable.key(course_name, prefix: 'courses'))
104
+ say_info "The course \"#{course_name}\" was successfully deleted."
105
+ end
106
+ else
107
+ system 'belajar courses help delete'
108
+ end
109
+ end
110
+
111
+ private
112
+
113
+ def courses_list_text(courses)
114
+ if courses.empty?
115
+ text = Terminal.text :courses_empty
116
+ else
117
+ text = [
118
+ "Available belajar courses:\n",
119
+ *courses.map { |course| "* #{File.basename(course.path)}\n" }
120
+ ].join("\n")
121
+ end
122
+
123
+ "#{text}\n#{Terminal.text :hint_course_download}"
124
+ end
125
+
126
+ def store_repo_data(user_and_repo)
127
+ parts = (user_and_repo ||= Belajar.config.initial_course).split('/')
128
+ author = parts.first
129
+ course = parts.second
130
+
131
+ course = Course.new(course)
132
+ QuickStore.store.set(course.key(:author), author)
133
+ QuickStore.store.set(course.key(:github), user_and_repo)
134
+ end
135
+
136
+ def scaffold_solutions
137
+ generator = Generator.new
138
+ generator.prepare
139
+ generator.scaffold(Belajar.config.courses_path, Belajar.config.solutions_path)
140
+ end
141
+
142
+ def print_download_warning(url, text)
143
+ message = [
144
+ "Error while downloading course from URL \"#{url}\"",
145
+ "#{text}\n",
146
+ Terminal.text(:hint_course_download)
147
+ ].join("\n")
148
+
149
+ say_warning message
150
+ end
151
+
152
+ def update_course(course)
153
+ url = QuickStore.store.get(course.key(:url))
154
+ github_repo = QuickStore.store.get(course.key(:github))
155
+ updated = GithubClient.updated?(github_repo)
156
+
157
+ if !github_repo || updated
158
+ download(url, 'updated') if url
159
+ else
160
+ say_info "Course \"#{course.title}\" is still up to date."
161
+ end
162
+ end
163
+
164
+ def print_course_not_available(course_name)
165
+ text = [
166
+ "The course \"#{course_name}\" is not available in",
167
+ "\"#{Belajar.config.courses_path}\".\n",
168
+ ]
169
+
170
+ say_warning text.join("\n")
171
+
172
+ unless Loading::Courses.load(Belajar.config.courses_path).empty?
173
+ Terminal::Courses.new.list
174
+ end
175
+ end
176
+ end
177
+
178
+ end
179
+ end
@@ -0,0 +1,78 @@
1
+ require 'thor'
2
+ require 'active_support/concern'
3
+ require 'colorize'
4
+
5
+ module Belajar
6
+ module Terminal
7
+
8
+ module Output
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ private
13
+
14
+ def say(text)
15
+ output = text.split("\n").map {|line| "\t#{line}" }.join("\n")
16
+ $stdout.puts output
17
+ end
18
+
19
+ def empty_line
20
+ $stdout.puts ''
21
+ end
22
+
23
+ def get(string)
24
+ $stdout.print "\n\t#{string} "
25
+ $stdin.gets.strip
26
+ end
27
+
28
+ def say_info(text)
29
+ say_box(text, ' ℹ', :light_blue)
30
+ end
31
+
32
+ def say_warning(text)
33
+ say_box(text, '⚠ ', :light_red)
34
+ end
35
+
36
+ def say_box(text, symbol, color)
37
+ empty_line
38
+ say line.send(color)
39
+ empty_line
40
+
41
+ indented_text = text.split("\n").join("\n#{' ' * (symbol.length + 1)}")
42
+ say indented_text.prepend("#{symbol} ").send(color)
43
+
44
+ empty_line
45
+ say line.send(color)
46
+ empty_line
47
+ end
48
+
49
+ def line(symbol = '-')
50
+ symbol * 70
51
+ end
52
+
53
+ def get_command(command, description)
54
+ say description
55
+
56
+ loop do
57
+ cmd = get '>'
58
+
59
+ unless cmd == command
60
+ say "This was something else. Try \"#{command}\"."
61
+ next
62
+ end
63
+
64
+ system cmd
65
+ break
66
+ end
67
+ end
68
+
69
+ def get_confirm(description)
70
+ say_warning description
71
+ confirm = get '(yes|no)'
72
+ yield if confirm == 'yes' && block_given?
73
+ end
74
+ end
75
+ end
76
+
77
+ end
78
+ end