sprig 0.1.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 (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