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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 391a106a3a06f570e669016d9625f92c62feb152
4
+ data.tar.gz: d8ffa472a8d094540db22d9016eff0ef685c8755
5
+ SHA512:
6
+ metadata.gz: 489f198c7432cab1d2474eec6939e1e67f4ef9ebdb43f3faa2c93fe96263b43797dd8372c360a8f65a7d23c724a4b806ea44335cc3923b45c16222a17edacfde
7
+ data.tar.gz: a15a56f4af8df7492fa7ac2212223c59314163620d28e0301dbbe55f66418dabebd2139e78fcf9b289566c62dc8ab9c0fc58afc7be9d2cdb07125a445a49de62
@@ -0,0 +1,20 @@
1
+ Copyright 2013 Viget
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,131 @@
1
+ #Sprig
2
+
3
+ [![Code Climate](https://codeclimate.com/github/vigetlabs/sprig.png)](https://codeclimate.com/github/vigetlabs/sprig) [![Build Status](https://travis-ci.org/vigetlabs/sprig.png?branch=master)](https://travis-ci.org/vigetlabs/sprig)
4
+
5
+ Seed Rails application by convention, not configuration.
6
+
7
+ Provides support for common files types: *csv*, *yaml*, and *json*. Extensible for the rest!
8
+
9
+
10
+ ##The Sprig Directive
11
+
12
+ Within your seed file, you can use the `sprig` directive to initiate Sprig's dark magicks. A simple directive might look like this.
13
+
14
+ ```ruby
15
+ # seeds.rb
16
+
17
+ include Sprig::Helpers
18
+
19
+ sprig [User, Post, Comment]
20
+ ```
21
+
22
+ This directive tells Sprig to go find your datafiles for the `User`, `Post`, and `Comment` seed resources, build records from the data entries, and insert them into the database. Sprig will automatically detect known datafile types like `.yml`, `.json`, or `.csv` within your environment-specific seed directory.
23
+
24
+
25
+ ##Environment
26
+
27
+ Seed files are unique to the environment in which your Rails application is running. Within `db/seeds` create an environment-specific directory (i.e. `/development` for your 'development' environment).
28
+
29
+ Todo: [Support for shared seed files]
30
+
31
+
32
+ ##Seed files
33
+
34
+ Hang your seed definitions on a `records` key for *yaml* and *json* files.
35
+
36
+ Examples:
37
+
38
+ ```yaml
39
+ # users.yml
40
+
41
+ records:
42
+ - sprig_id: 1
43
+ first_name: 'Lawson'
44
+ last_name: 'Kurtz'
45
+ username: 'lawdawg'
46
+ - sprig_id: 2
47
+ first_name: 'Ryan'
48
+ last_name: 'Foster'
49
+ username: 'mc_rubs'
50
+ ```
51
+
52
+ ```json
53
+ // posts.json
54
+
55
+ {
56
+ "records":[
57
+ {
58
+ "sprig_id":1,
59
+ "title":"Json title",
60
+ "content":"Json content"
61
+ },
62
+ {
63
+ "sprig_id":2,
64
+ "title":"Headline",
65
+ "content":"Words about things"
66
+ }
67
+ ]
68
+ }
69
+ ```
70
+
71
+ Each seed record needs a `sprig_id` defined that must be *unique across all seed files per class*. It can be an integer, string, whatever you prefer; as long as it is unique, Sprig can sort your seeds for insertion and detect any cyclic relationships.
72
+
73
+ Create relationships between seed records with the `sprig_record` helper:
74
+
75
+ ```yaml
76
+ # comments.yml
77
+
78
+ records:
79
+ - sprig_id: 1
80
+ post_id: "<%= sprig_record(Post, 1).id %>"
81
+ body: "Yaml Comment body"
82
+ ```
83
+
84
+ ### Special Options
85
+
86
+ These are provided in a `options:` key for *yaml* and *json* files.
87
+
88
+ #### find_existing_by:
89
+
90
+ Rather than starting from a blank database, you can optionally choose to find existing records and update them with seed data.
91
+
92
+ The passed in attribute or array of attributes will be used for finding existing records during the sprigging process.
93
+
94
+ Example:
95
+
96
+ ```yaml
97
+ # posts.yml
98
+
99
+ options:
100
+ find_existing_by: ['title', 'user_id']
101
+ ```
102
+
103
+ ##Custom Sources and Parsers
104
+
105
+ If all your data is in `.wat` files, fear not. You can tell Sprig where to look for your data, and point it toward a custom parser class for turning your data into records. The example below tells Sprig to read `User` seed data from a Google Spreadsheet, and parse it accordingly.
106
+
107
+ ```ruby
108
+ fanciness = {
109
+ :class => User,
110
+ :source => open('https://spreadsheets.google.com/feeds/list/somerandomtoken/1/public/values?alt=json'),
111
+ :parser => Sprig::Data::Parser::GoogleSpreadsheetJson
112
+ }
113
+
114
+ sprig [
115
+ fanciness,
116
+ Post,
117
+ Comment
118
+ ]
119
+ ```
120
+
121
+ ##Configuration
122
+
123
+ When Sprig conventions don't suit, just add a configuration block to your seed file.
124
+
125
+ ```ruby
126
+ Sprig.configure do |c|
127
+ c.directory = 'seed_files'
128
+ end
129
+ ```
130
+
131
+ This project rocks and uses MIT-LICENSE.
@@ -0,0 +1,5 @@
1
+ require 'rspec/core/rake_task'
2
+
3
+ RSpec::Core::RakeTask.new(:spec)
4
+
5
+ task :default => :spec
@@ -0,0 +1,29 @@
1
+ module Sprig
2
+ autoload :Configuration, 'sprig/configuration'
3
+ autoload :Planter, 'sprig/planter'
4
+ autoload :Dependency, 'sprig/dependency'
5
+ autoload :DependencyCollection, 'sprig/dependency_collection'
6
+ autoload :DependencySorter, 'sprig/dependency_sorter'
7
+ autoload :Directive, 'sprig/directive'
8
+ autoload :DirectiveList, 'sprig/directive_list'
9
+ autoload :Source, 'sprig/source'
10
+ autoload :Parser, 'sprig/parser'
11
+ autoload :Helpers, 'sprig/helpers'
12
+ autoload :Planter, 'sprig/planter'
13
+ autoload :SprigLogger, 'sprig/sprig_logger'
14
+ autoload :SprigRecordStore, 'sprig/sprig_record_store'
15
+ autoload :Data, 'sprig/data'
16
+ autoload :Seed, 'sprig/seed'
17
+
18
+ def self.configuration
19
+ @@configuration ||= Sprig::Configuration.new
20
+ end
21
+
22
+ def self.configure
23
+ yield configuration
24
+ end
25
+
26
+ def self.reset_configuration
27
+ @@configuration = nil
28
+ end
29
+ end
@@ -0,0 +1,16 @@
1
+ module Sprig
2
+ class Configuration
3
+
4
+ attr_writer :directory
5
+
6
+ def directory
7
+ Rails.root.join(@directory || default_directory, Rails.env)
8
+ end
9
+
10
+ private
11
+
12
+ def default_directory
13
+ 'db/seeds'
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,6 @@
1
+ module Sprig
2
+ module Data
3
+ autoload :Source, 'sprig/data/source'
4
+ autoload :Parser, 'sprig/data/parser'
5
+ end
6
+ end
@@ -0,0 +1,45 @@
1
+ module Sprig
2
+ class Dependency
3
+ attr_reader :id
4
+
5
+ def self.for(klass, sprig_id)
6
+ klass = to_klass(klass)
7
+ sprig_id = sprig_id.to_s
8
+
9
+ collection.get(klass, sprig_id) || new(klass, sprig_id)
10
+ end
11
+
12
+ def initialize(klass, sprig_id)
13
+ @klass = klass
14
+ @sprig_id = sprig_id
15
+ @id = SecureRandom.uuid
16
+
17
+ self.class.collection.set(klass, sprig_id, self)
18
+ end
19
+
20
+ def sprig_record_reference
21
+ "sprig_record(#{klass}, #{sprig_id})"
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :klass, :sprig_id
27
+
28
+ def self.to_klass(klass)
29
+ if klass.is_a?(String)
30
+ klass = klass.classify.constantize
31
+ end
32
+
33
+ raise ArgumentError, 'First argument must be a Class.' unless klass.is_a?(Class)
34
+
35
+ klass
36
+ rescue NameError => e
37
+ raise NameError, e.message
38
+ #TODO: rescue bad class references
39
+ end
40
+
41
+ def self.collection
42
+ @@collection ||= DependencyCollection.new
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,23 @@
1
+ module Sprig
2
+ class DependencyCollection
3
+ def initialize
4
+ @records = {}
5
+ end
6
+
7
+ def get(klass, id)
8
+ records_for_klass(klass)[id]
9
+ end
10
+
11
+ def set(klass, id, value)
12
+ records_for_klass(klass)[id] = value
13
+ end
14
+
15
+ private
16
+
17
+ attr_reader :records
18
+
19
+ def records_for_klass(klass)
20
+ records[klass] ||= {}
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,83 @@
1
+ module Sprig
2
+ class DependencySorter
3
+ class CircularDependencyError < StandardError; end
4
+
5
+ attr_reader :items
6
+
7
+ def initialize(items)
8
+ @items = items.to_a
9
+ end
10
+
11
+ def sorted_items
12
+ sorted_tags.map do |tag|
13
+ id_dictionary[tag]
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def id_dictionary
20
+ @id_dictionary ||= begin
21
+ {}.tap do |hash|
22
+ items.each do |item|
23
+ hash[item.dependency_id] = item
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ def dependency_hash
30
+ @dependency_hash ||= begin
31
+ TsortableHash.new.tap do |hash|
32
+ items.each do |item|
33
+ hash[item.dependency_id] = item.dependencies.map(&:id)
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ def sorted_tags
40
+ dependency_hash.tsort
41
+ rescue TSort::Cyclic => e
42
+ raise CircularDependencyError.new("Your sprig directives contain circular dependencies. #{e.message}")
43
+ rescue KeyError => key_error
44
+ raise missing_dependency_error_from_key_error(key_error)
45
+ end
46
+
47
+ def dependencies
48
+ items.map(&:dependencies).flatten
49
+ end
50
+
51
+ def missing_dependency_error_from_key_error(key_error)
52
+ key = key_error.message.match(/\Akey not found: "(.*)"\Z/)[1]
53
+ missing_dependency = dependencies.detect { |item| item.id == key }
54
+ MissingDependencyError.new(missing_dependency)
55
+ end
56
+
57
+ class TsortableHash < Hash
58
+ include TSort
59
+
60
+ alias tsort_each_node each_key
61
+
62
+ def tsort_each_child(node, &block)
63
+ fetch(node).each(&block)
64
+ end
65
+ end
66
+
67
+ class MissingDependencyError < StandardError
68
+ def initialize(missing_dependency = nil)
69
+ super message_for(missing_dependency)
70
+ end
71
+
72
+ private
73
+
74
+ def message_for(missing_dependency)
75
+ if missing_dependency.is_a? Dependency
76
+ "Undefined reference to '#{missing_dependency.sprig_record_reference}'"
77
+ else
78
+ "Referenced 'sprig_record' does not have a correlating record."
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,37 @@
1
+ module Sprig
2
+ class Directive
3
+ attr_reader :attributes
4
+
5
+ def initialize(args)
6
+ @attributes = begin
7
+ case
8
+ when args.is_a?(Hash)
9
+ args
10
+ when args < ActiveRecord::Base
11
+ { :class => args }
12
+ else
13
+ raise ArgumentError, argument_error_message
14
+ end
15
+ end
16
+ end
17
+
18
+ def klass
19
+ @klass ||= attributes.fetch(:class) { raise ArgumentError, argument_error_message }
20
+ end
21
+
22
+ def options
23
+ @options ||= attributes.except(:class)
24
+ end
25
+
26
+ def datasource
27
+ @datasource ||= Source.new(klass.to_s.tableize, options)
28
+ end
29
+
30
+ private
31
+
32
+ def argument_error_message
33
+ 'Sprig::Directive must be instantiated with an '\
34
+ 'ActiveRecord subclass or a Hash with :class defined'
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,30 @@
1
+ module Sprig
2
+ class DirectiveList
3
+
4
+ def initialize(definitions)
5
+ @definitions = Array(definitions)
6
+ end
7
+
8
+ def add_seeds_to_hopper(hopper)
9
+ seed_factories.each do |factory|
10
+ factory.add_seeds_to_hopper(hopper)
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ attr_reader :definitions
17
+
18
+ def directives
19
+ @directives ||= definitions.map do |definition|
20
+ Directive.new(definition)
21
+ end
22
+ end
23
+
24
+ def seed_factories
25
+ directives.map do |directive|
26
+ Seed::Factory.new_from_directive(directive)
27
+ end
28
+ end
29
+ end
30
+ end