data_files 1.0.0.rc1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 38ca97e41fc75a1a3799b5647fe95a25811515d81f8d84346635f190f46d0a3b
4
+ data.tar.gz: 29adfa22da840b9f96d9c10631140b3dcbd9d9c4f8d82c1895873efaee56db27
5
+ SHA512:
6
+ metadata.gz: 05bb9c56584c52153dbd84d95b24bb58dbeac9c2aec14237d6c3cedc20749f290c0ddfc0a097c2c14fca2ffea83aba240feaa3943e6f6daa7c02caa07b43327f
7
+ data.tar.gz: 3daec9182bc98e4036447bb73ed5d523b42dc9eb94cdf097df9de1e20bd1d2999cb70231bce9098e9ee6cb98b4257303ef4961b89f1559a5c7d68f693941f20d
@@ -0,0 +1,2 @@
1
+ Layout/LineLength:
2
+ Max: 100
@@ -0,0 +1 @@
1
+ 2.7.0
@@ -0,0 +1,3 @@
1
+ ## [1.0.0.rc1] - 2020-01-09
2
+ ### Added
3
+ - Initial version.
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in data-files.gemspec
4
+ gemspec
5
+
6
+ gem "rake", "~> 12.0"
7
+ gem "minitest", "~> 5.0"
@@ -0,0 +1,21 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ data_files (1.0.0.rc1)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ minitest (5.13.0)
10
+ rake (12.3.3)
11
+
12
+ PLATFORMS
13
+ ruby
14
+
15
+ DEPENDENCIES
16
+ data_files!
17
+ minitest (~> 5.0)
18
+ rake (~> 12.0)
19
+
20
+ BUNDLED WITH
21
+ 2.1.4
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) [2020] [Andreas Zecher]
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,139 @@
1
+ # REPL for Middleman Data Files
2
+
3
+ Written during Lab Week in January 2020 at Mynewsdesk.
4
+
5
+ This interactive shell allows users to manipulate [Middleman Data Files](https://middlemanapp.com/advanced/data-files/) with an API similar to ActiveRecord.
6
+
7
+ ## Getting Started
8
+
9
+ To start the interactive shell run the following command from your Middleman project directory:
10
+
11
+ ```
12
+ > rake data_files
13
+ ```
14
+
15
+ ## Querying data
16
+
17
+ Given a file located in `data/games.yml` in a Middleman project directory, we can query our data in different ways:
18
+
19
+ ```ruby
20
+ > Game.first
21
+ #<Game title: "A Light In Chorus", url: "http://www.alightinchorus.com", year: nil, _id: 1>
22
+
23
+ > Game.last
24
+ #<Game title: "Xenon 2: Megablast", url: "http://www.bitmap-brothers.co.uk/our-games/past/xenon2.htm", year: 1989, _id: 11>
25
+
26
+ > Game.find_by(title: "Another World")
27
+ #<Game title: "Another World", url: "http://www.anotherworld.fr/anotherworld_uk/", year: 1991, _id: 4>
28
+
29
+ > Game.where(year: 1991)
30
+ [#<Game title: "Another World", url: "http://www.anotherworld.fr/anotherworld_uk/", year: 1991, _id: 4>, #<Game title: "Commander Keen in Goodbye, Galaxy", url: "http://legacy.3drealms.com/keen4/", year: 1991, _id: 7>]
31
+
32
+ > Game.all.count
33
+ 10
34
+ ```
35
+
36
+ The internal `_id` attribute is ephemeral and can change between different sessions. It is not saved to the YAML file. It can however be used for querying:
37
+
38
+ ```ruby
39
+ > Game.find_by(_id: 12)
40
+ #<Game title: "Super Mario Maker 2", url: "https://www.nintendo.com/games/detail/super-mario-maker-2-switch/", year: 2019, _id: 12>
41
+ ```
42
+
43
+ ## Creating new data
44
+
45
+ We can add new items to our data file:
46
+
47
+ ```ruby
48
+ > game = Game.new(title: "Super Mario Maker 2", year: 2019, url: "https://www.nintendo.com/games/detail/super-mario-maker-2-switch/")
49
+ #<Game title: "Super Mario Maker 2", url: "https://www.nintendo.com/games/detail/super-mario-maker-2-switch/", year: 2019, _id: nil>
50
+
51
+ > game.save
52
+ true
53
+
54
+ > game
55
+ #<Game title: "Super Mario Maker 2", url: "https://www.nintendo.com/games/detail/super-mario-maker-2-switch/", year: 2019, _id: 12>
56
+ ```
57
+
58
+ ## Updating data
59
+
60
+ We can also update exisiting items:
61
+
62
+ ```ruby
63
+ > game = Game.where(year: nil).first
64
+ #<Game title: "A Light In Chorus", url: "http://www.alightinchorus.com", year: nil, _id: 1>
65
+
66
+ > game.year = 2020
67
+ 2020
68
+
69
+ > game.save
70
+ true
71
+
72
+ > game
73
+ #<Game title: "A Light In Chorus", url: "http://www.alightinchorus.com", year: 2020, _id: 1>
74
+ ```
75
+
76
+ ## Normalizing data
77
+
78
+ Items will ordered in the YAML file by their primary key. The first key in the array in the YAML file is considered the primary key. In our example the primary key is `title`:
79
+
80
+ ```yaml
81
+ ---
82
+ - title: A Light In Chorus
83
+ url: http://www.alightinchorus.com
84
+ year:
85
+ ```
86
+
87
+ Leading and trailing whitespace is automatically removed from string attributes on `save`:
88
+
89
+ ```ruby
90
+ > game = Game.new(title: " Bubble Bobble ")
91
+ #<Game title: " Bubble Bobble ", url: nil, year: nil, _id: nil>
92
+
93
+ > game.save
94
+ true
95
+
96
+ > game
97
+ #<Game title: "Bubble Bobble", url: nil, year: nil, _id: 11>
98
+ ```
99
+
100
+ ## Validation
101
+
102
+ Data will be automatically be validated on `save`. The validation logic is derived from the exisiting values in the YAML files.
103
+
104
+ ```ruby
105
+ > list = List.new(title: nil, user: 1, slug: false, ordered: "yes", featured: "no", published_at: "today", games: "A Light In Chorus, Advanced Wars")
106
+ #<List title: nil, user: 1, slug: false, ordered: "yes", featured: "no", published_at: "today", games: "A Light In Chorus, Advanced Wars", _id: nil>
107
+
108
+ > list.save
109
+ false
110
+
111
+ > list.errors
112
+ ["title must be string", "user must be string", "slug must be string", "ordered must be false or true", "featured must be false or true", "published_at must be date", "games must be array"]
113
+ ```
114
+
115
+ Here's an example for a valid `List` item. See [test/data/lists.yml](https://github.com/pixelate/data_files/blob/master/test/data/lists.yml) for the data structure that the validation logic is derived from.
116
+
117
+ ```ruby
118
+ > list = List.new(title: "A list", user: "andreaszecher", slug: "a-list", ordered: true, featured: false, published_at: Date.today, games: [{title: "A Light In Chorus"}, {title: "Advanced Wars"}])
119
+ #<List title: "A list", user: "andreaszecher", slug: "a-list", ordered: true, featured: false, published_at: 2020-01-09, games: [{:title=>"A Light In Chorus"}, {:title=>"Advanced Wars"}], _id: nil>
120
+ > list.valid?
121
+ true
122
+ > list.errors
123
+ []
124
+ > list.save
125
+ true
126
+ ```
127
+
128
+ Primary keys must be unique within a YAML file:
129
+
130
+ ```ruby
131
+ > game = Game.new(title: 'Another World')
132
+ #<Game title: "Another World", url: nil, year: nil, _id: nil>
133
+
134
+ > game.valid?
135
+ false
136
+
137
+ > game.errors
138
+ ["Game with title Another World already exists"]
139
+ ```
@@ -0,0 +1,19 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+ require_relative "lib/data_files/repl"
4
+
5
+ Rake::TestTask.new(:test) do |t|
6
+ t.libs << "test"
7
+ t.libs << "lib"
8
+ t.test_files = FileList["test/**/*_test.rb"]
9
+ end
10
+
11
+ task :"data_files" do |t|
12
+ unless Dir.exist?(File.join(Dir.pwd, 'data'))
13
+ puts 'Could not find data directory in working directory.'
14
+ exit
15
+ end
16
+
17
+ data_files = DataFiles::REPL.new(Dir.pwd)
18
+ data_files.prompt
19
+ end
@@ -0,0 +1,27 @@
1
+ require_relative 'lib/data_files/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "data_files"
5
+ spec.version = DataFiles::VERSION
6
+ spec.authors = ["Andreas Zecher"]
7
+ spec.email = ["andreas@polylists.com"]
8
+
9
+ spec.summary = %q{REPL for Middleman Data Files}
10
+ spec.description = %q{This interactive shell allows users to manipulate Middleman Data Files with an API similar to ActiveRecord.}
11
+ spec.homepage = "https://github.com/pixelate/data-files"
12
+ spec.license = "MIT"
13
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
14
+
15
+ spec.metadata["homepage_uri"] = spec.homepage
16
+ spec.metadata["source_code_uri"] = "https://github.com/pixelate/data-files"
17
+ spec.metadata["changelog_uri"] = "https://github.com/pixelate/data-files/blob/master/CHANGELOG.md"
18
+
19
+ # Specify which files should be added to the gem when it is released.
20
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
21
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
22
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
23
+ end
24
+ spec.bindir = "exe"
25
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
26
+ spec.require_paths = ["lib"]
27
+ end
@@ -0,0 +1,31 @@
1
+ ---
2
+ - title: A Light In Chorus
3
+ url: http://www.alightinchorus.com
4
+ year:
5
+ - title: Advance Wars
6
+ url: https://www.nintendo.co.jp/n08/bgwj/index.html
7
+ year: 2001
8
+ - title: 'Animal Crossing: New Leaf'
9
+ url: http://animal-crossing.com/newleaf/
10
+ year: 2012
11
+ - title: Another World
12
+ url: http://www.anotherworld.fr/anotherworld_uk/
13
+ year: 1991
14
+ - title: BOXBOY!
15
+ url: https://www.hallab.co.jp/eng/works/detail/002704/
16
+ year: 2015
17
+ - title: Commander Keen in Goodbye, Galaxy
18
+ url: http://legacy.3drealms.com/keen4/
19
+ year: 1991
20
+ - title: Donut County
21
+ url: http://www.donutcounty.com/
22
+ year: 2018
23
+ - title: Firewatch
24
+ url: http://www.firewatchgame.com
25
+ year: 2016
26
+ - title: wipE'out
27
+ url:
28
+ year: 1995
29
+ - title: 'Xenon 2: Megablast'
30
+ url: http://www.bitmap-brothers.co.uk/our-games/past/xenon2.htm
31
+ year: 1989
@@ -0,0 +1,18 @@
1
+ ---
2
+ - title: A list
3
+ user: andreaszecher
4
+ slug: a-list
5
+ ordered: true
6
+ featured: false
7
+ published_at: 2020-01-08
8
+ games:
9
+ - title: A Light In Chorus
10
+ - title: Advance Wars
11
+ - title: Another World
12
+ - title: 'Animal Crossing: New Leaf'
13
+ - title: BOXBOY!
14
+ - title: Commander Keen in Goodbye, Galaxy
15
+ - title: Donut County
16
+ - title: Firewatch
17
+ - title: wipE'out
18
+ - title: 'Xenon 2: Megablast'
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Base class for data querying and manipulation.
4
+ module DataFiles
5
+ class ActiveData
6
+ attr_reader :errors
7
+
8
+ def self.all
9
+ data.collect do |item|
10
+ new(item)
11
+ end
12
+ end
13
+
14
+ def self.first
15
+ new(data.first)
16
+ end
17
+
18
+ def self.last
19
+ new(data.last)
20
+ end
21
+
22
+ def self.where(conditions)
23
+ all.select do |item|
24
+ selected = true
25
+ conditions.each do |key, value|
26
+ selected = false if item.send(key) != value
27
+ end
28
+ selected
29
+ end
30
+ end
31
+
32
+ def self.find_by(conditions)
33
+ results = where(conditions)
34
+ if results.size.positive?
35
+ results.first
36
+ else
37
+ nil
38
+ end
39
+ end
40
+
41
+ def self.write_yaml
42
+ sort_by_primary_key
43
+ item_attributes = all.collect(&:attributes)
44
+
45
+ File.open("data/#{name.downcase}s.yml", 'w') do |file|
46
+ file.write(item_attributes.to_yaml)
47
+ end
48
+ end
49
+
50
+ def self.data
51
+ class_variable_get(:@@data)
52
+ end
53
+
54
+ def self.data=(value)
55
+ class_variable_set(:@@data, value)
56
+ end
57
+
58
+ def self.attributes
59
+ class_variable_get(:@@attributes)
60
+ end
61
+
62
+ def self.types
63
+ class_variable_get(:@@types)
64
+ end
65
+
66
+ def self.sort_by_primary_key
67
+ self.data = data.sort_by do |item|
68
+ primary_key_value = item.values.first
69
+ if primary_key_value.is_a? String
70
+ primary_key_value.downcase
71
+ else
72
+ primary_key_value
73
+ end
74
+ end
75
+ end
76
+
77
+ def initialize(attrs = {})
78
+ @_id = nil
79
+ attrs.each { |key, value| send("#{key}=", value) }
80
+ end
81
+
82
+ def attributes
83
+ attributes_hash = {}
84
+ self.class.attributes.each do |attr|
85
+ attributes_hash[attr] = send(attr) unless attr == '_id'
86
+ end
87
+ attributes_hash
88
+ end
89
+
90
+ def to_s
91
+ joined_attributes = self.class.attributes.collect do |attr|
92
+ val = send(attr)
93
+ val = "\"#{val}\"" if val.is_a? String
94
+ "#{attr}: #{val.nil? ? 'nil' : val}"
95
+ end.join(', ')
96
+
97
+ "#<#{self.class} #{joined_attributes}>"
98
+ end
99
+
100
+ def inspect
101
+ to_s
102
+ end
103
+
104
+ def valid?
105
+ @errors = []
106
+
107
+ primary_key = self.class.attributes.first
108
+ primary_key_values = self.class.data.collect do |item|
109
+ { item['_id'] => item[primary_key] }
110
+ end
111
+
112
+ primary_key_values.each do |item|
113
+ item.each do |key, value|
114
+ if key != @_id && value == send(primary_key)
115
+ @errors << "#{self.class.name} with #{primary_key} #{send(primary_key)} already exists"
116
+ end
117
+ end
118
+ end
119
+
120
+ attributes.each do |key, value|
121
+ unless self.class.types[key].include?(value.class.name)
122
+ @errors << type_validation_error_message(key, self.class.types[key])
123
+ end
124
+ end
125
+
126
+ @errors.size.zero?
127
+ end
128
+
129
+ def save
130
+ return false unless valid?
131
+
132
+ strip
133
+
134
+ self.class.data = self.class.data.map do |item|
135
+ if item['_id'] == @_id
136
+ attributes.merge('_id' => @_id)
137
+ else
138
+ item
139
+ end
140
+ end
141
+
142
+ if @_id.nil?
143
+ @_id = next_id
144
+ self.class.data << attributes.merge('_id' => @_id)
145
+ end
146
+
147
+ self.class.write_yaml
148
+ true
149
+ end
150
+
151
+ def strip
152
+ self.class.attributes.each do |attr|
153
+ send("#{attr}=", send(attr).strip) if send(attr).is_a? String
154
+ end
155
+ self
156
+ end
157
+
158
+ private
159
+
160
+ def type_validation_error_message(attr, class_names)
161
+ allowed_types = class_names.map do |class_name|
162
+ class_name.gsub('Class', '').downcase
163
+ end
164
+
165
+ "#{attr} must be #{allowed_types.sort.join(', ')}".sub(/.*\K, /, ' or ')
166
+ end
167
+
168
+ def next_id
169
+ self.class.data.collect { |item| item['_id'] }.compact.max + 1
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+ require 'readline'
5
+ require 'yaml'
6
+ require_relative 'active_data.rb'
7
+
8
+ # Loads all yaml files in given directory, creates ActiveData subclass
9
+ # for each file and provides an interactive shell.
10
+ module DataFiles
11
+ class REPL
12
+ def initialize(directory)
13
+ @klass_names = []
14
+ parse_data(directory)
15
+ end
16
+
17
+ def parse_data(directory)
18
+ Dir.foreach(File.join(directory, 'data')) do |filename|
19
+ next unless filename.end_with?('.yml')
20
+
21
+ filepath = File.join(directory, 'data', filename)
22
+ key = File.basename(filepath, File.extname(filepath))
23
+ data = load_yaml(filepath)
24
+
25
+ klass_name = key.capitalize.delete_suffix('s')
26
+ create_class(klass_name, data)
27
+ end
28
+ end
29
+
30
+ def create_class(klass_name, data)
31
+ @klass_names << klass_name
32
+
33
+ types = parse_types(data)
34
+ klass = Class.new(ActiveData) do
35
+ class_variable_set(:@@data, data)
36
+ class_variable_set(:@@attributes, data.first.keys)
37
+ class_variable_set(:@@types, types)
38
+ attr_accessor(*data.first.keys)
39
+ end
40
+
41
+ Object.const_set(klass_name, klass)
42
+ end
43
+
44
+ def prompt
45
+ initial_prompt
46
+ read_input
47
+ end
48
+
49
+ def initial_prompt
50
+ puts 'Available data models:'
51
+ @klass_names.sort.each do |klass_name|
52
+ puts " - #{klass_name}"
53
+ end
54
+ end
55
+
56
+ def read_input
57
+ bnd = binding
58
+ while (input = Readline.readline('> ', true))
59
+ begin
60
+ puts bnd.eval(input).to_s
61
+ rescue StandardError => e
62
+ puts "\e[31m#{e.class}:\e[0m #{e.message}"
63
+ end
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ def load_yaml(filepath)
70
+ YAML
71
+ .safe_load(File.read(filepath), permitted_classes: [Date])
72
+ .map
73
+ .each_with_index { |item, index| item.merge('_id' => index + 1) }
74
+ end
75
+
76
+ def parse_types(data)
77
+ types = {}
78
+ data.first.keys.each do |attr|
79
+ types[attr] = data.collect { |item| item[attr].class.name }
80
+ types[attr] << 'TrueClass' if types[attr].include?('FalseClass')
81
+ types[attr] << 'FalseClass' if types[attr].include?('TrueClass')
82
+ types[attr].uniq!
83
+ end
84
+ types
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,3 @@
1
+ module DataFiles
2
+ VERSION = "1.0.0.rc1"
3
+ end
metadata ADDED
@@ -0,0 +1,61 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: data_files
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0.rc1
5
+ platform: ruby
6
+ authors:
7
+ - Andreas Zecher
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2020-01-09 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: This interactive shell allows users to manipulate Middleman Data Files
14
+ with an API similar to ActiveRecord.
15
+ email:
16
+ - andreas@polylists.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - ".rubocop.yml"
22
+ - ".ruby-version"
23
+ - CHANGELOG.md
24
+ - Gemfile
25
+ - Gemfile.lock
26
+ - LICENSE.txt
27
+ - README.md
28
+ - Rakefile
29
+ - data-files.gemspec
30
+ - data/games.yml
31
+ - data/lists.yml
32
+ - lib/data_files/active_data.rb
33
+ - lib/data_files/repl.rb
34
+ - lib/data_files/version.rb
35
+ homepage: https://github.com/pixelate/data-files
36
+ licenses:
37
+ - MIT
38
+ metadata:
39
+ homepage_uri: https://github.com/pixelate/data-files
40
+ source_code_uri: https://github.com/pixelate/data-files
41
+ changelog_uri: https://github.com/pixelate/data-files/blob/master/CHANGELOG.md
42
+ post_install_message:
43
+ rdoc_options: []
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: 2.3.0
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">"
54
+ - !ruby/object:Gem::Version
55
+ version: 1.3.1
56
+ requirements: []
57
+ rubygems_version: 3.1.2
58
+ signing_key:
59
+ specification_version: 4
60
+ summary: REPL for Middleman Data Files
61
+ test_files: []