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.
- 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
|
+

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