mumukit-sync 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +12 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +5 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +49 -0
  9. data/Rakefile +6 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +8 -0
  12. data/lib/mumukit/sync.rb +28 -0
  13. data/lib/mumukit/sync/inflator.rb +14 -0
  14. data/lib/mumukit/sync/inflator/choice.rb +11 -0
  15. data/lib/mumukit/sync/inflator/exercise.rb +11 -0
  16. data/lib/mumukit/sync/inflator/gobstones_kids_boards.rb +35 -0
  17. data/lib/mumukit/sync/inflator/multiple_choice.rb +15 -0
  18. data/lib/mumukit/sync/inflator/single_choice.rb +12 -0
  19. data/lib/mumukit/sync/store.rb +12 -0
  20. data/lib/mumukit/sync/store/bibliotheca.rb +23 -0
  21. data/lib/mumukit/sync/store/github.rb +59 -0
  22. data/lib/mumukit/sync/store/github/bot.rb +76 -0
  23. data/lib/mumukit/sync/store/github/exercise_builder.rb +19 -0
  24. data/lib/mumukit/sync/store/github/git_lib.rb +14 -0
  25. data/lib/mumukit/sync/store/github/guide_builder.rb +43 -0
  26. data/lib/mumukit/sync/store/github/guide_export.rb +50 -0
  27. data/lib/mumukit/sync/store/github/guide_import.rb +14 -0
  28. data/lib/mumukit/sync/store/github/guide_reader.rb +88 -0
  29. data/lib/mumukit/sync/store/github/guide_writer.rb +89 -0
  30. data/lib/mumukit/sync/store/github/licenses/COPYRIGHT.txt.erb +5 -0
  31. data/lib/mumukit/sync/store/github/licenses/LICENSE.txt +428 -0
  32. data/lib/mumukit/sync/store/github/licenses/README.md.erb +6 -0
  33. data/lib/mumukit/sync/store/github/operation.rb +47 -0
  34. data/lib/mumukit/sync/store/github/ordering.rb +23 -0
  35. data/lib/mumukit/sync/store/github/schema.rb +123 -0
  36. data/lib/mumukit/sync/store/github/schema/exercise.rb +37 -0
  37. data/lib/mumukit/sync/store/github/schema/guide.rb +28 -0
  38. data/lib/mumukit/sync/store/github/with_file_reading.rb +19 -0
  39. data/lib/mumukit/sync/store/json.rb +27 -0
  40. data/lib/mumukit/sync/store/thesaurus.rb +19 -0
  41. data/lib/mumukit/sync/syncer.rb +63 -0
  42. data/lib/mumukit/sync/version.rb +5 -0
  43. data/mumukit-sync.gemspec +34 -0
  44. metadata +198 -0
@@ -0,0 +1,6 @@
1
+ ## License
2
+ ![License icon](https://licensebuttons.net/l/by-sa/3.0/88x31.png)
3
+
4
+ This content is distributed under Creative Commons License Share-Alike, 4.0. [https://creativecommons.org/licenses/by-sa/4.0/](https://creativecommons.org/licenses/by-sa/4.0)
5
+
6
+ <%= @copyright %>
@@ -0,0 +1,47 @@
1
+ class Mumukit::Sync::Store::Github
2
+ class Operation
3
+ attr_accessor :bot
4
+
5
+ def initialize(options)
6
+ @bot = options[:bot]
7
+ @web_hook_base_url = options[:web_hook_base_url]
8
+ end
9
+
10
+ def with_local_repo(&block)
11
+ Dir.mktmpdir("mumuki.#{self.class.name}") do |dir|
12
+ bot.clone_into repo, dir, &block
13
+ end
14
+ end
15
+
16
+ def can_run?
17
+ true
18
+ end
19
+
20
+ def run!
21
+ return unless can_run?
22
+
23
+ puts "#{self.class.name} : running before run hook for repository #{repo}"
24
+ before_run_in_local_repo
25
+
26
+ result = nil
27
+ with_local_repo do |dir, local_repo|
28
+ puts "#{self.class.name} : running run hook for repository #{repo}"
29
+ result = run_in_local_repo dir, local_repo
30
+ end
31
+
32
+ puts "#{self.class.name} : running after run hook repository #{repo}"
33
+ ensure_post_commit_hook!
34
+ result
35
+ end
36
+
37
+ def ensure_post_commit_hook!
38
+ bot.register_post_commit_hook!(repo, @web_hook_base_url) if bot.authenticated? && @web_hook_base_url
39
+ end
40
+
41
+ def before_run_in_local_repo
42
+ end
43
+
44
+ def run_in_local_repo(dir, local_repo)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,23 @@
1
+ class Mumukit::Sync::Store::Github
2
+ module Ordering
3
+ def self.from(order)
4
+ order ? FixedOrdering.new(order) : NaturalOrdering
5
+ end
6
+ end
7
+
8
+ class FixedOrdering
9
+ def initialize(order)
10
+ @order = order
11
+ end
12
+
13
+ def position_for(id)
14
+ @order.index(id) + 1
15
+ end
16
+ end
17
+
18
+ module NaturalOrdering
19
+ def self.position_for(id)
20
+ id
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,123 @@
1
+ class Mumukit::Sync::Store::Github
2
+ ## Schema definition explanation
3
+ #
4
+ # * name: the name of the field
5
+ # * kind: the type of the field: metadata, special, file or transient.
6
+ # * metadata fields are those that are small and fit into the metadata.yml when exported to git
7
+ # * special fields are those are essentials and not part of any file, like the id or name when exported to git
8
+ # * transient fields are not exported to git
9
+ # * file fields are large fields that are exported to git within their own file.
10
+ # * reverse: the name of the field in the model. By default, it is assumed to be the same of name, but
11
+ # can be overridden with this option
12
+ # * default: the default value of the field
13
+ # * extension: the file extension. It only applies to file kinds. It can be a plain extension or one of the following
14
+ # special extensions:
15
+ # * test: the extension of the test framework
16
+ # * code: the normal extension for the language
17
+ #
18
+ module Schema
19
+ def defaults
20
+ fields.map { |it| [it.reverse_name, it.default] }.to_h.compact
21
+ end
22
+
23
+ def metadata_fields
24
+ fields.select { |it| it.kind == :metadata }
25
+ end
26
+
27
+ def simple_fields
28
+ fields.select { |it| [:special, :file].include? it.kind }
29
+ end
30
+
31
+ def file_fields
32
+ fields.select { |it| it.kind == :file }
33
+ end
34
+
35
+ def fields
36
+ @field ||= fields_schema.map { |it| new_field(it) }
37
+ end
38
+
39
+ def slice(json)
40
+ json.slice(*fields.map(&:reverse_name))
41
+ end
42
+
43
+ def yaml_hash
44
+ struct to: proc(&:to_yaml),
45
+ from: proc { |path| YAML.load_file(path) }
46
+ end
47
+
48
+ def yaml_list(key)
49
+ struct to: proc { |it| {key => it.map(&:stringify_keys)}.to_yaml },
50
+ from: proc { |path| YAML.load_file(path).try { |it| it[key] } }
51
+ end
52
+
53
+ def name
54
+ with { |it| it&.dig(:name) }
55
+ end
56
+
57
+ def with(&block)
58
+ struct to: block, from: proc { |it| File.read(it) }
59
+ end
60
+
61
+ private
62
+
63
+ def new_field(it)
64
+ Field.new(it)
65
+ end
66
+
67
+ class Field < OpenStruct
68
+ def reverse_name
69
+ reverse || name
70
+ end
71
+
72
+ def safe_transform
73
+ transform || struct(to: proc { |it| it }, from: proc { |it| File.read(it) })
74
+ end
75
+
76
+ ## Writing fields to Github
77
+
78
+ def get_file_name(language)
79
+ "#{name}.#{get_file_extension(language)}"
80
+ end
81
+
82
+ def get_field_value(document)
83
+ safe_transform.to.call document[reverse_name]
84
+ end
85
+
86
+ def field_value_present?(document)
87
+ document[reverse_name].present?
88
+ end
89
+
90
+ def get_file_extension(language)
91
+ case extension
92
+ when :code then
93
+ language[:extension]
94
+ when :test then
95
+ language[:test_extension]
96
+ else
97
+ extension
98
+ end
99
+ end
100
+
101
+ ## Reading fields from Github
102
+
103
+ def find_file_name(root)
104
+ files = Dir.glob("#{root}/#{name}.*")
105
+ if files.length == 1
106
+ files[0]
107
+ elsif files.empty? && required
108
+ raise "Missing #{name} file"
109
+ else
110
+ nil
111
+ end
112
+ end
113
+
114
+ def read_field_file(root)
115
+ find_file_name(root).try { |it| safe_transform.from.call it }
116
+ end
117
+
118
+ end
119
+ end
120
+ end
121
+
122
+ require_relative './schema/exercise'
123
+ require_relative './schema/guide'
@@ -0,0 +1,37 @@
1
+ module Mumukit::Sync::Store::Github::Schema::Exercise
2
+ extend Mumukit::Sync::Store::Github::Schema
3
+
4
+ def self.fields_schema
5
+ [
6
+ {name: :id, kind: :special},
7
+ {name: :name, kind: :special},
8
+
9
+ {name: :tags, kind: :metadata, reverse: :tag_list, transform: with { |it| it.to_a }},
10
+ {name: :layout, kind: :metadata},
11
+ {name: :editor, kind: :metadata},
12
+
13
+ {name: :type, kind: :metadata},
14
+ {name: :extra_visible, kind: :metadata},
15
+ {name: :language, kind: :metadata, transform: name },
16
+ {name: :teacher_info, kind: :metadata},
17
+ {name: :manual_evaluation, kind: :metadata},
18
+ {name: :choices, kind: :metadata},
19
+
20
+ {name: :expectations, kind: :file, extension: 'yml', transform: yaml_list('expectations')},
21
+ {name: :assistance_rules, kind: :file, extension: 'yml', transform: yaml_list('rules')},
22
+ {name: :randomizations, kind: :file, extension: 'yml', transform: yaml_hash},
23
+
24
+ {name: :goal, kind: :metadata},
25
+ {name: :test, kind: :file, extension: :test},
26
+ {name: :extra, kind: :file, extension: :code},
27
+ {name: :default, kind: :file, extension: :code, reverse: :default_content},
28
+
29
+ {name: :description, kind: :file, extension: 'md', required: true},
30
+ {name: :hint, kind: :file, extension: 'md'},
31
+ {name: :corollary, kind: :file, extension: 'md'},
32
+ {name: :initial_state, kind: :file, extension: 'md'},
33
+ {name: :final_state, kind: :file, extension: 'md'},
34
+ {name: :free_form_editor_source, kind: :file, extension: 'html'}
35
+ ]
36
+ end
37
+ end
@@ -0,0 +1,28 @@
1
+ module Mumukit::Sync::Store::Github::Schema::Guide
2
+ extend Mumukit::Sync::Store::Github::Schema
3
+
4
+ def self.fields_schema
5
+ [
6
+ {name: :exercises, kind: :special},
7
+ {name: :id, kind: :special},
8
+ {name: :slug, kind: :special},
9
+
10
+ {name: :name, kind: :metadata},
11
+ {name: :locale, kind: :metadata},
12
+ {name: :type, kind: :metadata},
13
+ {name: :beta, kind: :metadata},
14
+ {name: :teacher_info, kind: :metadata},
15
+ {name: :language, kind: :metadata, transform: name },
16
+ {name: :id_format, kind: :metadata},
17
+ {name: :order, kind: :metadata, transform: with { |it| it.map { |e| e[:id] } }, reverse: :exercises},
18
+ {name: :private, kind: :metadata},
19
+ {name: :expectations},
20
+
21
+ {name: :description, kind: :file, extension: 'md', required: true},
22
+ {name: :corollary, kind: :file, extension: 'md'},
23
+ {name: :extra, kind: :file, extension: :code},
24
+ {name: :AUTHORS, kind: :file, extension: 'txt', reverse: :authors},
25
+ {name: :COLLABORATORS, kind: :file, extension: 'txt', reverse: :collaborators}
26
+ ]
27
+ end
28
+ end
@@ -0,0 +1,19 @@
1
+ require 'yaml'
2
+
3
+ class Mumukit::Sync::Store::Github
4
+ module WithFileReading
5
+ def read_code_file(root, filename)
6
+ files = Dir.glob("#{root}/#{filename}.*")
7
+ file = files[0]
8
+ read_file(file) if files.length == 1
9
+ end
10
+
11
+ def read_yaml_file(path)
12
+ YAML.load_file(path) if path && File.exist?(path)
13
+ end
14
+
15
+ def read_file(path)
16
+ File.read(path) if path && File.exist?(path)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,27 @@
1
+ module Mumukit::Sync::Store
2
+ class Json
3
+ def initialize(json)
4
+ @json = json
5
+ end
6
+
7
+ def sync_keys
8
+ raise 'Non-discoverable store'
9
+ end
10
+
11
+ def read_resource(sync_key)
12
+ post_transform sync_key.kind, pre_transform(sync_key.kind, @json).deep_symbolize_keys
13
+ end
14
+
15
+ def write_resource!(*)
16
+ raise 'Read-only store'
17
+ end
18
+
19
+ def pre_transform(kind, json)
20
+ json
21
+ end
22
+
23
+ def post_transform(kind, json)
24
+ json
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,19 @@
1
+ module Mumukit::Sync::Store
2
+ class Thesaurus
3
+ def initialize(thesaurus_bridge)
4
+ @thesaurus_bridge = thesaurus_bridge
5
+ end
6
+ def sync_keys
7
+ @thesaurus_bridge.runners.map { |it| Mumukit::Sync.key(:language, it) }
8
+ end
9
+
10
+ def read_resource(sync_key)
11
+ return unless sync_key.kind == :language
12
+ Mumukit::Bridge::Runner.new(runner_url).importable_info
13
+ end
14
+
15
+ def write_resource!(*)
16
+ raise 'Read-only store'
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,63 @@
1
+ ## An import and export pipeline for generic resources.
2
+ ##
3
+ ## A resource - that is, something that can be imported or exported - must implement the following methods:
4
+ ##
5
+ ## * #sync_key: returns a kind-id pair created using `Mumukit::Sync.key`, used to locate resources within a store
6
+ ## * #to_resource_h: returns a canonical hash representation of the resource. Only required by `Mumukit::Sync#export!`
7
+ ## * #import_from_resource_h!: populates and saves the resource with its canonical hash representation
8
+ ## * .locate_resource(resource_id): finds or initializes the resource given its resource id. Only required by `Mumukit::Sync#import_all!`
9
+
10
+ module Mumukit::Sync
11
+ class Syncer
12
+ def initialize(store, inflators = [], resource_classifier = nil)
13
+ @store = store
14
+ @inflators = inflators
15
+ @resource_classifier ||= proc { |kind| Mumukit::Sync.constantize(kind) }
16
+ end
17
+
18
+ def sync_keys_matching(id_regex = nil)
19
+ id_regex ||= /.*/
20
+ @store.sync_keys.select { |key| id_regex.matches? key.id }
21
+ end
22
+
23
+ def import_all!(id_regex = nil)
24
+ sync_keys_matching(id_regex).each do |key|
25
+ puts "Importing #{key.kind} #{key.id}"
26
+ begin
27
+ locate_and_import! key
28
+ rescue => e
29
+ puts "Ignoring #{key.id} because of import error #{e}"
30
+ end
31
+ end
32
+ end
33
+
34
+ def locate_and_import!(*args)
35
+ locate(key_for(*args)).tap { |it| import! it }
36
+ end
37
+
38
+ def import!(resource)
39
+ resource_h = @store.read_resource(resource.sync_key)
40
+ Mumukit::Sync::Inflator.inflate_with! resource.sync_key, resource_h, @inflators
41
+ resource.import_from_resource_h!(resource_h)
42
+ end
43
+
44
+ def locate_and_export!(*args)
45
+ locate(key_for(*args)).tap { |it| export! it }
46
+ end
47
+
48
+ def export!(resource)
49
+ resource_h = resource.to_resource_h
50
+ @store.write_resource!(resource.sync_key, resource_h)
51
+ end
52
+
53
+ private
54
+
55
+ def locate(key)
56
+ @resource_classifier.call(key.kind).locate_resource(key.id)
57
+ end
58
+
59
+ def key_for(*args)
60
+ args.size == 1 ? args.first : Mumukit::Sync.key(*args)
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,5 @@
1
+ module Mumukit
2
+ module Sync
3
+ VERSION = "0.0.0"
4
+ end
5
+ end
@@ -0,0 +1,34 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "mumukit/sync/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "mumukit-sync"
8
+ spec.version = Mumukit::Sync::VERSION
9
+ spec.authors = ["Franco Bulgarelli"]
10
+ spec.email = ["franco@mumuki.org"]
11
+
12
+ spec.summary = %q{Synchronization tool for resources}
13
+ spec.description = %q{Library for importing and exporting things within Mumuki}
14
+ spec.homepage = 'https://mumuki.org'
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = "exe"
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.add_dependency 'mumukit-core', '~> 1.7'
25
+ spec.add_dependency 'mumukit-bridge', '~> 3.5'
26
+ spec.add_dependency 'mumukit-auth', '~> 7.0'
27
+
28
+ spec.add_dependency 'git', '~> 1.5'
29
+ spec.add_dependency 'octokit', '~> 4.1'
30
+
31
+ spec.add_development_dependency "bundler", "~> 1.16"
32
+ spec.add_development_dependency "rake", "~> 10.0"
33
+ spec.add_development_dependency "rspec", "~> 3.0"
34
+ end