sprig 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +131 -0
  4. data/Rakefile +5 -0
  5. data/lib/sprig.rb +29 -0
  6. data/lib/sprig/configuration.rb +16 -0
  7. data/lib/sprig/data.rb +6 -0
  8. data/lib/sprig/dependency.rb +45 -0
  9. data/lib/sprig/dependency_collection.rb +23 -0
  10. data/lib/sprig/dependency_sorter.rb +83 -0
  11. data/lib/sprig/directive.rb +37 -0
  12. data/lib/sprig/directive_list.rb +30 -0
  13. data/lib/sprig/helpers.rb +25 -0
  14. data/lib/sprig/parser.rb +9 -0
  15. data/lib/sprig/parser/base.rb +15 -0
  16. data/lib/sprig/parser/csv.rb +22 -0
  17. data/lib/sprig/parser/google_spreadsheet_json.rb +35 -0
  18. data/lib/sprig/parser/json.rb +9 -0
  19. data/lib/sprig/parser/yml.rb +9 -0
  20. data/lib/sprig/planter.rb +39 -0
  21. data/lib/sprig/seed.rb +9 -0
  22. data/lib/sprig/seed/attribute.rb +54 -0
  23. data/lib/sprig/seed/attribute_collection.rb +33 -0
  24. data/lib/sprig/seed/entry.rb +80 -0
  25. data/lib/sprig/seed/factory.rb +56 -0
  26. data/lib/sprig/seed/record.rb +35 -0
  27. data/lib/sprig/source.rb +143 -0
  28. data/lib/sprig/sprig_logger.rb +68 -0
  29. data/lib/sprig/sprig_record_store.rb +31 -0
  30. data/lib/sprig/version.rb +3 -0
  31. data/spec/db/activerecord.db +0 -0
  32. data/spec/feature/configurations_spec.rb +30 -0
  33. data/spec/fixtures/cassettes/google_spreadsheet_json_posts.yml +60 -0
  34. data/spec/fixtures/models/comment.rb +5 -0
  35. data/spec/fixtures/models/post.rb +2 -0
  36. data/spec/fixtures/seeds/staging/posts.yml +6 -0
  37. data/spec/fixtures/seeds/test/comments.yml +6 -0
  38. data/spec/fixtures/seeds/test/legacy_posts.yml +6 -0
  39. data/spec/fixtures/seeds/test/posts.csv +2 -0
  40. data/spec/fixtures/seeds/test/posts.json +1 -0
  41. data/spec/fixtures/seeds/test/posts.yml +6 -0
  42. data/spec/fixtures/seeds/test/posts_find_existing_by_multiple.yml +9 -0
  43. data/spec/fixtures/seeds/test/posts_find_existing_by_single.yml +8 -0
  44. data/spec/fixtures/seeds/test/posts_missing_dependency.yml +7 -0
  45. data/spec/lib/sprig/configuration_spec.rb +21 -0
  46. data/spec/lib/sprig/directive_list_spec.rb +24 -0
  47. data/spec/lib/sprig/directive_spec.rb +60 -0
  48. data/spec/spec_helper.rb +75 -0
  49. data/spec/sprig_spec.rb +205 -0
  50. metadata +211 -0
@@ -0,0 +1,25 @@
1
+ module Sprig
2
+ module Helpers
3
+ def seed_directory
4
+ Sprig.configuration.directory
5
+ end
6
+
7
+ def sprig_environment
8
+ Rails.env #TODO: make customizable
9
+ end
10
+
11
+ def sprig(directive_definitions)
12
+ hopper = []
13
+ DirectiveList.new(directive_definitions).add_seeds_to_hopper(hopper)
14
+ Planter.new(hopper).sprig
15
+ end
16
+
17
+ def sprig_record(klass, seed_id)
18
+ SprigRecordStore.instance.get(klass, seed_id)
19
+ end
20
+
21
+ def sprig_file(relative_path)
22
+ File.new(seed_directory.join('files', relative_path))
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,9 @@
1
+ module Sprig
2
+ module Parser
3
+ autoload :Base, 'sprig/parser/base'
4
+ autoload :Csv, 'sprig/parser/csv'
5
+ autoload :Json, 'sprig/parser/json'
6
+ autoload :Yml, 'sprig/parser/yml'
7
+ autoload :GoogleSpreadsheetJson, 'sprig/parser/google_spreadsheet_json'
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ module Sprig
2
+ module Parser
3
+ class Base
4
+ attr_reader :data_io
5
+
6
+ def initialize(data_io)
7
+ @data_io = data_io
8
+ end
9
+
10
+ def parse
11
+ raise NotImplementedError, 'Parsers must implement #parse'
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,22 @@
1
+ require 'csv'
2
+
3
+ module Sprig
4
+ module Parser
5
+ class Csv < Base
6
+
7
+ def parse
8
+ { :records => records }
9
+ end
10
+
11
+ private
12
+
13
+ def records
14
+ [].tap do |records|
15
+ CSV.foreach(data_io, headers: :first_row, skip_blanks: true) do |row|
16
+ records << row.to_hash
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,35 @@
1
+ module Sprig
2
+ module Parser
3
+ class GoogleSpreadsheetJson < Base
4
+
5
+ def parse
6
+ { :records => records }
7
+ end
8
+
9
+ private
10
+
11
+ def records
12
+ @records ||= raw_records.map { |record| build_record(record) }
13
+ end
14
+
15
+ def build_record(record)
16
+ hash = {}
17
+
18
+ record.keys.each do |key|
19
+ attr_name = key.tr('-', '_').scan(/gsx\$([a-z_]+$)/).flatten.first
20
+ hash[attr_name] = record[key]['$t'] if attr_name
21
+ end
22
+
23
+ hash
24
+ end
25
+
26
+ def raw_records
27
+ json['feed']['entry']
28
+ end
29
+
30
+ def json
31
+ @json ||= JSON.load(data_io)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,9 @@
1
+ module Sprig
2
+ module Parser
3
+ class Json < Base
4
+ def parse
5
+ JSON.load(data_io)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Sprig
2
+ module Parser
3
+ class Yml < Base
4
+ def parse
5
+ YAML.load(data_io)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,39 @@
1
+ module Sprig
2
+ class Planter
3
+ def initialize(seeds)
4
+ @seeds = seeds.to_a
5
+ end
6
+
7
+ def sprig
8
+ dependency_sorted_seeds.each do |seed|
9
+ plant(seed)
10
+ end
11
+
12
+ logger.log_summary
13
+ end
14
+
15
+ private
16
+
17
+ attr_reader :seeds
18
+
19
+ def dependency_sorted_seeds
20
+ DependencySorter.new(seeds).sorted_items
21
+ end
22
+
23
+ def logger
24
+ @logger ||= SprigLogger.new
25
+ end
26
+
27
+ def plant(seed)
28
+ logger.processing
29
+ seed.before_save
30
+
31
+ if seed.save_record
32
+ seed.save_to_store
33
+ logger.log_success(seed)
34
+ else
35
+ logger.log_error(seed)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,9 @@
1
+ module Sprig
2
+ module Seed
3
+ autoload :Attribute, 'sprig/seed/attribute'
4
+ autoload :AttributeCollection, 'sprig/seed/attribute_collection'
5
+ autoload :Entry, 'sprig/seed/entry'
6
+ autoload :Factory, 'sprig/seed/factory'
7
+ autoload :Record, 'sprig/seed/record'
8
+ end
9
+ end
@@ -0,0 +1,54 @@
1
+ module Sprig
2
+ module Seed
3
+ class Attribute
4
+ attr_reader :name, :raw_value, :value
5
+
6
+ def initialize(name, raw_value)
7
+ @name = name.to_s
8
+ @raw_value = raw_value
9
+ end
10
+
11
+ def dependencies
12
+ @dependencies ||= determine_dependencies.uniq
13
+ end
14
+
15
+ def value
16
+ compute_value if @value.nil?
17
+
18
+ @value
19
+ end
20
+
21
+ private
22
+
23
+ def determine_dependencies
24
+ if computed_value?
25
+ matches = raw_value.scan(/(sprig_record\(([A-Z][^,]*), ([\d]*)\))+/)
26
+ matches.map {|match| Dependency.for(match[1], match[2]) }
27
+ else
28
+ []
29
+ end
30
+ end
31
+
32
+ def string?
33
+ raw_value.is_a?(String)
34
+ end
35
+
36
+ def computed_value?
37
+ string? && raw_value =~ computed_value_regex
38
+ end
39
+
40
+ def computed_value_regex
41
+ /<%[=]?(.*)%>/
42
+ end
43
+
44
+ def compute_value
45
+ @value = if computed_value?
46
+ matches = computed_value_regex.match(raw_value)
47
+ eval(matches[1])
48
+ else
49
+ raw_value
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,33 @@
1
+ module Sprig
2
+ module Seed
3
+ class AttributeCollection
4
+ include Enumerable
5
+
6
+ delegate :each, :to => :attributes
7
+
8
+ def initialize(attrs_hash)
9
+ @attrs_hash = attrs_hash.to_hash
10
+ end
11
+
12
+ def find_by_name(name)
13
+ attributes.detect {|attribute| attribute.name == name.to_s } || attribute_not_found(name)
14
+ end
15
+
16
+ private
17
+
18
+ attr_reader :attrs_hash
19
+
20
+ class AttributeNotFoundError < StandardError; end
21
+
22
+ def attributes
23
+ @attributes ||= attrs_hash.map do |name, value|
24
+ Attribute.new(name, value)
25
+ end
26
+ end
27
+
28
+ def attribute_not_found(name)
29
+ raise AttributeNotFoundError, "Attribute '#{name}' is not present."
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,80 @@
1
+ module Sprig
2
+ module Seed
3
+ class Entry
4
+ def initialize(klass, attrs, options)
5
+ self.klass = klass
6
+ attrs = attrs.to_hash.with_indifferent_access
7
+ self.sprig_id = attrs.delete(:sprig_id) || SecureRandom.uuid
8
+ @attributes = AttributeCollection.new(attrs)
9
+ @options = options
10
+ end
11
+
12
+ def dependencies
13
+ @dependencies ||= attributes.map do |attribute|
14
+ attribute.dependencies
15
+ end.flatten.uniq
16
+ end
17
+
18
+ def dependency_id
19
+ @dependency_id ||= Dependency.for(klass, sprig_id).id
20
+ end
21
+
22
+ def before_save
23
+ # TODO: make these filters take chains like rails before_filters
24
+ if options[:delete_existing_by]
25
+ klass.delete_all(options[:delete_existing_by] => attributes.find_by_name(options[:delete_existing_by]).value)
26
+ end
27
+ end
28
+
29
+ def save_record
30
+ record.save
31
+ end
32
+
33
+ def save_to_store
34
+ SprigRecordStore.instance.save(record.orm_record, sprig_id)
35
+ end
36
+
37
+ def success_log_text
38
+ "#{klass.name} with sprig_id #{sprig_id} successfully saved."
39
+ end
40
+
41
+ def error_log_text
42
+ "There was an error saving #{klass.name} with sprig_id #{sprig_id}."
43
+ end
44
+
45
+ def record
46
+ @record ||= new_or_existing_record
47
+ end
48
+
49
+ private
50
+
51
+ attr_reader :attributes, :klass, :options, :sprig_id
52
+
53
+ def klass=(klass)
54
+ raise ArgumentError, 'First argument must be a Class' unless klass.is_a?(Class)
55
+
56
+ @klass = klass
57
+ end
58
+
59
+ def sprig_id=(sprig_id)
60
+ @sprig_id = sprig_id.to_s
61
+ end
62
+
63
+ def new_or_existing_record
64
+ if options[:find_existing_by]
65
+ Record.new_or_existing(klass, attributes, find_existing_params)
66
+ else
67
+ Record.new(klass, attributes)
68
+ end
69
+ end
70
+
71
+ def find_existing_params
72
+ Array(options[:find_existing_by]).inject({}) do |hash, attribute_name|
73
+ hash.merge!(
74
+ { attribute_name => attributes.find_by_name(attribute_name).value }
75
+ )
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,56 @@
1
+ module Sprig
2
+ module Seed
3
+ class Factory
4
+ def self.new_from_directive(directive)
5
+ raise ArgumentError, 'Must provide a Directive' unless directive.is_a? Directive
6
+
7
+ klass = directive.klass
8
+ datasource = directive.datasource
9
+ options = directive.options
10
+
11
+ new(klass, datasource, options)
12
+ end
13
+
14
+ def initialize(klass, datasource, options)
15
+ self.klass = klass
16
+ self.datasource = datasource
17
+ self.initial_options = options
18
+ end
19
+
20
+ def add_seeds_to_hopper(hopper)
21
+ datasource.records.each do |record_data|
22
+ hopper << Entry.new(klass, record_data, options)
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :datasource, :initial_options, :klass
29
+
30
+ def klass=(klass)
31
+ raise ArgumentError, 'Must provide a Class as first argument' unless klass.is_a? Class
32
+
33
+ @klass = klass
34
+ end
35
+
36
+ def datasource=(datasource)
37
+ raise ArgumentError, 'Datasource must respond to #records and #options' unless datasource.respond_to?(:records) && datasource.respond_to?(:options)
38
+
39
+ @datasource = datasource
40
+ end
41
+
42
+ def initial_options=(initial_options)
43
+ initial_options ||= {}
44
+ @initial_options = initial_options.to_hash
45
+ end
46
+
47
+ def options
48
+ @options ||= datasource.options.merge(initial_options)
49
+ end
50
+
51
+ def data
52
+ @data ||= datasource.to_hash
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,35 @@
1
+ module Sprig
2
+ module Seed
3
+ class Record
4
+ attr_reader :orm_record
5
+
6
+ delegate :errors, :to => :orm_record
7
+
8
+ def self.new_or_existing(klass, attributes, find_params)
9
+ orm_record = klass.where(find_params).first
10
+ new(klass, attributes, orm_record)
11
+ end
12
+
13
+ def initialize(klass, attributes, orm_record = nil)
14
+ @klass = klass
15
+ @attributes = attributes
16
+ @orm_record = orm_record || klass.new
17
+ end
18
+
19
+ def save
20
+ populate_attributes
21
+ orm_record.save
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :attributes, :klass
27
+
28
+ def populate_attributes
29
+ attributes.each do |attribute|
30
+ orm_record.send(:"#{attribute.name}=", attribute.value)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end