mumukit-sync 0.0.0

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 (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