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