mumukit-sync 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +12 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +49 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/mumukit/sync.rb +28 -0
- data/lib/mumukit/sync/inflator.rb +14 -0
- data/lib/mumukit/sync/inflator/choice.rb +11 -0
- data/lib/mumukit/sync/inflator/exercise.rb +11 -0
- data/lib/mumukit/sync/inflator/gobstones_kids_boards.rb +35 -0
- data/lib/mumukit/sync/inflator/multiple_choice.rb +15 -0
- data/lib/mumukit/sync/inflator/single_choice.rb +12 -0
- data/lib/mumukit/sync/store.rb +12 -0
- data/lib/mumukit/sync/store/bibliotheca.rb +23 -0
- data/lib/mumukit/sync/store/github.rb +59 -0
- data/lib/mumukit/sync/store/github/bot.rb +76 -0
- data/lib/mumukit/sync/store/github/exercise_builder.rb +19 -0
- data/lib/mumukit/sync/store/github/git_lib.rb +14 -0
- data/lib/mumukit/sync/store/github/guide_builder.rb +43 -0
- data/lib/mumukit/sync/store/github/guide_export.rb +50 -0
- data/lib/mumukit/sync/store/github/guide_import.rb +14 -0
- data/lib/mumukit/sync/store/github/guide_reader.rb +88 -0
- data/lib/mumukit/sync/store/github/guide_writer.rb +89 -0
- data/lib/mumukit/sync/store/github/licenses/COPYRIGHT.txt.erb +5 -0
- data/lib/mumukit/sync/store/github/licenses/LICENSE.txt +428 -0
- data/lib/mumukit/sync/store/github/licenses/README.md.erb +6 -0
- data/lib/mumukit/sync/store/github/operation.rb +47 -0
- data/lib/mumukit/sync/store/github/ordering.rb +23 -0
- data/lib/mumukit/sync/store/github/schema.rb +123 -0
- data/lib/mumukit/sync/store/github/schema/exercise.rb +37 -0
- data/lib/mumukit/sync/store/github/schema/guide.rb +28 -0
- data/lib/mumukit/sync/store/github/with_file_reading.rb +19 -0
- data/lib/mumukit/sync/store/json.rb +27 -0
- data/lib/mumukit/sync/store/thesaurus.rb +19 -0
- data/lib/mumukit/sync/syncer.rb +63 -0
- data/lib/mumukit/sync/version.rb +5 -0
- data/mumukit-sync.gemspec +34 -0
- 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,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
|