silly 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +8 -0
- data/README.md +200 -0
- data/Rakefile +23 -0
- data/features/cascade.feature +0 -0
- data/features/data_file.feature +82 -0
- data/features/path.feature +38 -0
- data/features/sort.feature +42 -0
- data/features/step_defs.rb +57 -0
- data/features/support/env.rb +9 -0
- data/features/support/helpers.rb +59 -0
- data/features/where.feature +56 -0
- data/features/where_file.feature +72 -0
- data/lib/silly.rb +174 -0
- data/lib/silly/base_model.rb +10 -0
- data/lib/silly/collection.rb +9 -0
- data/lib/silly/data_model.rb +15 -0
- data/lib/silly/item.rb +73 -0
- data/lib/silly/page_model.rb +79 -0
- data/lib/silly/parse.rb +88 -0
- data/lib/silly/query_operators.rb +47 -0
- data/lib/silly/utils.rb +22 -0
- data/lib/silly/version.rb +3 -0
- data/silly.gemspec +24 -0
- metadata +117 -0
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,200 @@
|
|
1
|
+
[![Build Status](https://travis-ci.org/ruhoh/silly.png?branch=master)](https://travis-ci.org/ruhoh/silly)
|
2
|
+
|
3
|
+
Silly is a filesystem based Object Document Mapper.
|
4
|
+
Use it to query a directory like you would a database -- useful for static websites.
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
Silly is in alpha which means the API is unstable and still under development. Silly is/will-be used to power [ruhoh](http://ruhoh.com) 3.0. You can contribute and help stablize Silly by playing with it and providing feedback!
|
9
|
+
|
10
|
+
Install and run the development (head) version:
|
11
|
+
|
12
|
+
$ git@github.com:ruhoh/silly.git
|
13
|
+
|
14
|
+
Take note of where you downloaded the gem.
|
15
|
+
|
16
|
+
Navigate to a directory you want to query.
|
17
|
+
|
18
|
+
Create a file named `Gemfile` with the contents:
|
19
|
+
|
20
|
+
gem 'silly', :path => '/Users/jade/Dropbox/gems/silly'
|
21
|
+
|
22
|
+
Make sure to replace the path with where you downloaded the gem.
|
23
|
+
|
24
|
+
Install the bundle:
|
25
|
+
|
26
|
+
$ bundle install
|
27
|
+
|
28
|
+
## Usage
|
29
|
+
|
30
|
+
We can test the query API by loading an irb session loading Silly:
|
31
|
+
|
32
|
+
Load irb:
|
33
|
+
|
34
|
+
$ bundle exec irb
|
35
|
+
|
36
|
+
Load and instantiate silly relative to the current directory:
|
37
|
+
|
38
|
+
> require 'silly'
|
39
|
+
> query = Silly::Query.new
|
40
|
+
> query.append_path(Dir.pwd)
|
41
|
+
|
42
|
+
|
43
|
+
## Data
|
44
|
+
|
45
|
+
### Cascade
|
46
|
+
|
47
|
+
Append an arbitrary number of paths to enable a logical cascade where files overload one another of the same name.
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
query = Silly::Query.new
|
51
|
+
query.append_path(Dir.pwd)
|
52
|
+
query.append_path(File.join(Dir.pwd, "theme-folder"))
|
53
|
+
```
|
54
|
+
|
55
|
+
### Data Files
|
56
|
+
|
57
|
+
Data files are automatically parsed and merged down the cascade:
|
58
|
+
|
59
|
+
Given:
|
60
|
+
./data.yml
|
61
|
+
./theme-folder/data.json
|
62
|
+
|
63
|
+
query = Silly::Query.new
|
64
|
+
query.append_path(Dir.pwd)
|
65
|
+
query.append_path("theme-folder")
|
66
|
+
|
67
|
+
query.where("$shortname" => "data").first
|
68
|
+
|
69
|
+
### File metadata
|
70
|
+
|
71
|
+
Files have inherit metadata such as path, filename, extension etc.
|
72
|
+
Files can also define arbitrary in-file "top metadata" or FrontMatter made popular by static site generators like Jekyll.
|
73
|
+
|
74
|
+
**page.html**
|
75
|
+
|
76
|
+
---
|
77
|
+
title: "A custom title"
|
78
|
+
date: "2013/12/12"
|
79
|
+
tags:
|
80
|
+
- opinion
|
81
|
+
- tech
|
82
|
+
---
|
83
|
+
|
84
|
+
... content ...
|
85
|
+
|
86
|
+
|
87
|
+
## Query API
|
88
|
+
|
89
|
+
All queries return a [Silly::Collection](https://github.com/ruhoh/silly/blob/master/lib/silly/collection.rb) of [Silly::Item](https://github.com/ruhoh/silly/blob/master/lib/silly/item.rb)s.
|
90
|
+
|
91
|
+
`Silly::Item` represents a file.
|
92
|
+
|
93
|
+
`@item.data` lazily generates any metadata in the file.
|
94
|
+
|
95
|
+
`@item.content` lazily generates the raw content of the file.
|
96
|
+
|
97
|
+
|
98
|
+
### Path
|
99
|
+
|
100
|
+
**Return all files in the base directory (unnested)**
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
query = Silly::Query.new
|
104
|
+
query.append_path(Dir.pwd)
|
105
|
+
|
106
|
+
query.to_a
|
107
|
+
```
|
108
|
+
|
109
|
+
**Return files in the base directory and all sub-directories.**
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
query = Silly::Query.new
|
113
|
+
query.append_path(Dir.pwd)
|
114
|
+
|
115
|
+
query.path_all("").to_a
|
116
|
+
```
|
117
|
+
|
118
|
+
### Filter
|
119
|
+
|
120
|
+
**Return files with specific extension**
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
query = Silly::Query.new
|
124
|
+
query.append_path(Dir.pwd)
|
125
|
+
|
126
|
+
query.where("$ext" => ".md").to_a
|
127
|
+
```
|
128
|
+
|
129
|
+
**Return files contained in a given directory (can be any nesting level)**
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
query = Silly::Query.new
|
133
|
+
query.append_path(Dir.pwd)
|
134
|
+
|
135
|
+
query.where("$directories" => "drafts").to_a
|
136
|
+
```
|
137
|
+
|
138
|
+
**Return files where an attribute is a given value**
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
query = Silly::Query.new
|
142
|
+
query.append_path(Dir.pwd)
|
143
|
+
|
144
|
+
query.where("size" => "med").to_a
|
145
|
+
```
|
146
|
+
|
147
|
+
**Return files where an attribute is not a given value**
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
query = Silly::Query.new
|
151
|
+
query.append_path(Dir.pwd)
|
152
|
+
|
153
|
+
query.where("size" => {"$ne" => "med"}).to_a
|
154
|
+
```
|
155
|
+
|
156
|
+
**Return files where an attribute exists**
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
query = Silly::Query.new
|
160
|
+
query.append_path(Dir.pwd)
|
161
|
+
|
162
|
+
query.where("size" => {"$exists" => true}).to_a
|
163
|
+
```
|
164
|
+
|
165
|
+
|
166
|
+
### Sort
|
167
|
+
|
168
|
+
**Return files sorted by a given attribute**
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
query = Silly::Query.new
|
172
|
+
query.append_path(Dir.pwd)
|
173
|
+
|
174
|
+
query.sort([:date, :desc]).to_a
|
175
|
+
```
|
176
|
+
|
177
|
+
|
178
|
+
### Chaining
|
179
|
+
|
180
|
+
```ruby
|
181
|
+
query = Silly::Query.new
|
182
|
+
query.append_path(Dir.pwd)
|
183
|
+
|
184
|
+
query
|
185
|
+
.path("posts")
|
186
|
+
.where("$directories" => { "drafts" => { "$ne" => "drafts" } })
|
187
|
+
.sort([:date, :desc])
|
188
|
+
.to_a
|
189
|
+
```
|
190
|
+
|
191
|
+
## Why
|
192
|
+
|
193
|
+
Silly has no dependencies so it's a great engine to build custom static site generators on top of.
|
194
|
+
|
195
|
+
I really like the idea of a static website generator but it's really hard to get people to adopt your philosphy of how thing's should work so rather than do that why not empower anyone to build a static generator however they want! That's the idea anyway and this is a very early release so we'll see.
|
196
|
+
Also I am really inspired by projects like [Tire](https://github.com/karmi/retire) and [Mongoid](https://github.com/mongoid/mongoid) of which I've taken heavy inspiration from. Silly is a way to level up my gem skills B).
|
197
|
+
|
198
|
+
## License
|
199
|
+
|
200
|
+
[MIT](http://www.opensource.org/licenses/mit-license.html)
|
data/Rakefile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), *%w[lib]))
|
2
|
+
require 'rubygems'
|
3
|
+
require 'rake'
|
4
|
+
require 'bundler'
|
5
|
+
require 'ruhoh/version'
|
6
|
+
|
7
|
+
name = Dir['*.gemspec'].first.split('.').first
|
8
|
+
gemspec_file = "#{name}.gemspec"
|
9
|
+
gem_file = "#{name}-#{Silly::VERSION}.gem"
|
10
|
+
|
11
|
+
task :release => :build do
|
12
|
+
sh "git commit --allow-empty -m 'Release #{Silly::VERSION}'"
|
13
|
+
sh "git tag v#{Silly::VERSION}"
|
14
|
+
sh "git push origin master --tags"
|
15
|
+
sh "git push origin v#{Silly::VERSION}"
|
16
|
+
sh "gem push pkg/#{name}-#{Silly::VERSION}.gem"
|
17
|
+
end
|
18
|
+
|
19
|
+
task :build do
|
20
|
+
sh "mkdir -p pkg"
|
21
|
+
sh "gem build #{gemspec_file}"
|
22
|
+
sh "mv #{gem_file} pkg"
|
23
|
+
end
|
File without changes
|
@@ -0,0 +1,82 @@
|
|
1
|
+
Feature: Data File
|
2
|
+
I want to natively access the data in data files so I can use the data in my content
|
3
|
+
|
4
|
+
Scenario: Query data file
|
5
|
+
Given the file "person.yml" with body:
|
6
|
+
"""
|
7
|
+
---
|
8
|
+
name: "jade"
|
9
|
+
address:
|
10
|
+
city: "alhambra"
|
11
|
+
fruits:
|
12
|
+
- mango
|
13
|
+
- kiwi
|
14
|
+
"""
|
15
|
+
When I query the path ""
|
16
|
+
Then this query's first result should have the data:
|
17
|
+
"""
|
18
|
+
{
|
19
|
+
"name": "jade",
|
20
|
+
"address": {
|
21
|
+
"city": "alhambra"
|
22
|
+
},
|
23
|
+
"fruits": [
|
24
|
+
"mango",
|
25
|
+
"kiwi"
|
26
|
+
]
|
27
|
+
}
|
28
|
+
"""
|
29
|
+
|
30
|
+
Scenario: Query a merged data file
|
31
|
+
Given the file "person.yml" with body:
|
32
|
+
"""
|
33
|
+
---
|
34
|
+
name: "jade"
|
35
|
+
address:
|
36
|
+
city: "alhambra"
|
37
|
+
fruits:
|
38
|
+
- mango
|
39
|
+
- kiwi
|
40
|
+
"""
|
41
|
+
And the file "cascade/person.yml" with body:
|
42
|
+
"""
|
43
|
+
---
|
44
|
+
name: "Bob"
|
45
|
+
greeting: "Hai!"
|
46
|
+
"""
|
47
|
+
And I append the path "cascade" to the query
|
48
|
+
When I query the path ""
|
49
|
+
Then this query's first result should have the data:
|
50
|
+
"""
|
51
|
+
{
|
52
|
+
"name": "Bob",
|
53
|
+
"greeting" : "Hai!",
|
54
|
+
"address": {
|
55
|
+
"city": "alhambra"
|
56
|
+
},
|
57
|
+
"fruits": [
|
58
|
+
"mango",
|
59
|
+
"kiwi"
|
60
|
+
]
|
61
|
+
}
|
62
|
+
"""
|
63
|
+
|
64
|
+
Scenario: Query a merged data file with different formats
|
65
|
+
Given the file "person.json" with body:
|
66
|
+
"""
|
67
|
+
{ "name" : "Bob" }
|
68
|
+
"""
|
69
|
+
And the file "cascade/person.yml" with body:
|
70
|
+
"""
|
71
|
+
---
|
72
|
+
greeting: "Hai!"
|
73
|
+
"""
|
74
|
+
And I append the path "cascade" to the query
|
75
|
+
When I query the path ""
|
76
|
+
Then this query's first result should have the data:
|
77
|
+
"""
|
78
|
+
{
|
79
|
+
"name": "Bob",
|
80
|
+
"greeting" : "Hai!"
|
81
|
+
}
|
82
|
+
"""
|
@@ -0,0 +1,38 @@
|
|
1
|
+
Feature: Query Path
|
2
|
+
I want to scope a query to a path so I can better arrange my content for optimized user experience.
|
3
|
+
|
4
|
+
Scenario: Query with empty path (base path)
|
5
|
+
Given some files with values:
|
6
|
+
| file |
|
7
|
+
| apple.html |
|
8
|
+
| banana.html |
|
9
|
+
| cranberry.html |
|
10
|
+
When I query the path ""
|
11
|
+
Then this query returns the ordered results "apple.html, banana.html, cranberry.html"
|
12
|
+
|
13
|
+
Scenario: Query with path
|
14
|
+
Given some files with values:
|
15
|
+
| file |
|
16
|
+
| food/apple.html |
|
17
|
+
| food/banana.html |
|
18
|
+
| food/cranberry.html |
|
19
|
+
When I query the path "food"
|
20
|
+
Then this query returns the ordered results "food/apple.html, food/banana.html, food/cranberry.html"
|
21
|
+
|
22
|
+
Scenario: Query with path and nested files
|
23
|
+
Given some files with values:
|
24
|
+
| file |
|
25
|
+
| food/apple.html |
|
26
|
+
| food/cool/banana.html |
|
27
|
+
| food/cool/cranberry.html |
|
28
|
+
When I query the path "food"
|
29
|
+
Then this query returns the ordered results "food/apple.html"
|
30
|
+
|
31
|
+
Scenario: Query with path_all and nested files
|
32
|
+
Given some files with values:
|
33
|
+
| file |
|
34
|
+
| food/apple.html |
|
35
|
+
| food/cool/banana.html |
|
36
|
+
| food/cool/cranberry.html |
|
37
|
+
When I query the path_all "food"
|
38
|
+
Then this query returns the ordered results "food/apple.html, food/cool/banana.html, food/cool/cranberry.html"
|
@@ -0,0 +1,42 @@
|
|
1
|
+
Feature: Query Sort
|
2
|
+
I want to sort a query so I can better arrange my content for optimized user experience.
|
3
|
+
|
4
|
+
Scenario: Query with sort
|
5
|
+
Given some files with values:
|
6
|
+
| file |
|
7
|
+
| food/apple.html |
|
8
|
+
| food/banana.html |
|
9
|
+
| food/cranberry.html |
|
10
|
+
When I query the path "food"
|
11
|
+
And I sort the query by "id" "desc"
|
12
|
+
Then this query returns the ordered results "food/cranberry.html, food/banana.html, food/apple.html"
|
13
|
+
|
14
|
+
Scenario: Query with sort by metadata attribute
|
15
|
+
Given some files with values:
|
16
|
+
| file | order |
|
17
|
+
| food/apple.html | 2 |
|
18
|
+
| food/banana.html | 3 |
|
19
|
+
| food/cranberry.html | 1 |
|
20
|
+
When I query the path "food"
|
21
|
+
And I sort the query by "order" "asc"
|
22
|
+
Then this query returns the ordered results "food/cranberry.html, food/apple.html, food/banana.html"
|
23
|
+
|
24
|
+
Scenario: Query with sort by date attribute ascending
|
25
|
+
Given some files with values:
|
26
|
+
| file | date |
|
27
|
+
| essays/hello.md | 2013-12-01 |
|
28
|
+
| essays/zebra.md | 2013-12-10 |
|
29
|
+
| essays/apple.md | 2013-12-25 |
|
30
|
+
When I query the path "essays"
|
31
|
+
And I sort the query by "date" "asc"
|
32
|
+
Then this query returns the ordered results "essays/hello.md, essays/zebra.md, essays/apple.md"
|
33
|
+
|
34
|
+
Scenario: Query with sort by date attribute descending
|
35
|
+
Given some files with values:
|
36
|
+
| file | date |
|
37
|
+
| essays/hello.md | 2013-12-01 |
|
38
|
+
| essays/zebra.md | 2013-12-10 |
|
39
|
+
| essays/apple.md | 2013-12-25 |
|
40
|
+
When I query the path "essays"
|
41
|
+
And I sort the query by "date" "desc"
|
42
|
+
Then this query returns the ordered results "essays/apple.md, essays/zebra.md, essays/hello.md"
|
@@ -0,0 +1,57 @@
|
|
1
|
+
Transform(/^(should|should NOT)$/) do |matcher|
|
2
|
+
matcher.downcase.gsub(' ', '_')
|
3
|
+
end
|
4
|
+
|
5
|
+
When(/^I query the path "(.*?)"$/) do |term|
|
6
|
+
query.path(term)
|
7
|
+
end
|
8
|
+
|
9
|
+
When(/^I query the path_all "(.*?)"$/) do |term|
|
10
|
+
query.path_all(term)
|
11
|
+
end
|
12
|
+
|
13
|
+
When(/^I sort the query by "(.*?)" "(.*?)"$/) do |attribute, order|
|
14
|
+
query.sort([attribute, order])
|
15
|
+
end
|
16
|
+
|
17
|
+
When(/^I filter the query with:$/) do |json|
|
18
|
+
query.where(JSON.parse(json))
|
19
|
+
end
|
20
|
+
|
21
|
+
When(/^this query returns the ordered results "(.*?)"$/) do |results|
|
22
|
+
results = results.split(/[\s,]+/).map(&:strip)
|
23
|
+
query.map{ |a| a["id"] }.should == results
|
24
|
+
end
|
25
|
+
|
26
|
+
When(/^this query's first result should have the data:$/) do |json|
|
27
|
+
data = JSON.parse(json)
|
28
|
+
result = query.first
|
29
|
+
result.data.should == data
|
30
|
+
end
|
31
|
+
|
32
|
+
When(/^I append the path "(.*?)" to the query$/) do |path|
|
33
|
+
query.append_path(File.join(SampleSitePath, path))
|
34
|
+
end
|
35
|
+
|
36
|
+
Given(/^a config file with value:$/) do |string|
|
37
|
+
make_config(JSON.parse(string))
|
38
|
+
end
|
39
|
+
|
40
|
+
Given(/^a config file with values:$/) do |table|
|
41
|
+
data = table.rows_hash
|
42
|
+
data.each{ |key, value| data[key] = JSON.parse(value) }
|
43
|
+
make_config(data)
|
44
|
+
end
|
45
|
+
|
46
|
+
Given(/^the file "(.*)" with body:$/) do |file, body|
|
47
|
+
make_file(path: file, body: body)
|
48
|
+
end
|
49
|
+
|
50
|
+
Given(/^some files with values:$/) do |table|
|
51
|
+
table.hashes.each do |row|
|
52
|
+
file = row['file'] ; row.delete('file')
|
53
|
+
body = row['body'] ; row.delete('body')
|
54
|
+
|
55
|
+
make_file(path: file, data: row, body: body)
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
SampleSitePath = File.expand_path(File.join(File.dirname(__FILE__), '..', '..', '__tmp'))
|
2
|
+
|
3
|
+
def query
|
4
|
+
@query ||= new_query
|
5
|
+
end
|
6
|
+
|
7
|
+
def new_query
|
8
|
+
query = Silly::Query.new
|
9
|
+
query.append_path(SampleSitePath)
|
10
|
+
query
|
11
|
+
end
|
12
|
+
|
13
|
+
def make_config(data)
|
14
|
+
path = File.join(SampleSitePath, "config.yml")
|
15
|
+
File.open(path, "w+") { |file|
|
16
|
+
file.puts data.to_yaml
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
def make_file(opts)
|
21
|
+
path = File.join(SampleSitePath, opts[:path])
|
22
|
+
FileUtils.mkdir_p(File.dirname(path))
|
23
|
+
|
24
|
+
data = opts[:data] || {}
|
25
|
+
if data['categories']
|
26
|
+
data['categories'] = data['categories'].to_s.split(',').map(&:strip)
|
27
|
+
end
|
28
|
+
if data['tags']
|
29
|
+
data['tags'] = data['tags'].to_s.split(',').map(&:strip)
|
30
|
+
puts "tags #{data['tags']}"
|
31
|
+
end
|
32
|
+
data.delete('layout') if data['layout'].to_s.strip.empty?
|
33
|
+
|
34
|
+
metadata = data.empty? ? '' : data.to_yaml.to_s + "\n---\n"
|
35
|
+
|
36
|
+
File.open(path, "w+") { |file|
|
37
|
+
if metadata.empty?
|
38
|
+
file.puts <<-TEXT
|
39
|
+
#{ opts[:body] }
|
40
|
+
TEXT
|
41
|
+
else
|
42
|
+
file.puts <<-TEXT
|
43
|
+
#{ metadata }
|
44
|
+
|
45
|
+
#{ opts[:body] }
|
46
|
+
TEXT
|
47
|
+
end
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
Before do
|
52
|
+
remove_instance_variable(:@query) if (instance_variable_defined?(:@query))
|
53
|
+
FileUtils.remove_dir(SampleSitePath,1) if Dir.exists? SampleSitePath
|
54
|
+
Dir.mkdir SampleSitePath
|
55
|
+
end
|
56
|
+
|
57
|
+
After do
|
58
|
+
FileUtils.remove_dir(SampleSitePath,1) if Dir.exists? SampleSitePath
|
59
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
Feature: Query filter
|
2
|
+
I want to filter a query so I can better arrange my content for optimized user experience.
|
3
|
+
|
4
|
+
Scenario: Query with filter by metadata attribute
|
5
|
+
Given some files with values:
|
6
|
+
| file | type |
|
7
|
+
| food/apple.html | fruit |
|
8
|
+
| food/banana.html | fruit |
|
9
|
+
| food/cranberry.html | fruit |
|
10
|
+
| food/peanuts.html | nuts |
|
11
|
+
When I query the path "food"
|
12
|
+
And I filter the query with:
|
13
|
+
"""
|
14
|
+
{"type" : "nuts"}
|
15
|
+
"""
|
16
|
+
Then this query returns the ordered results "food/peanuts.html"
|
17
|
+
|
18
|
+
Scenario: Query with filter by 2 different metadata attributes
|
19
|
+
Given some files with values:
|
20
|
+
| file | type | size |
|
21
|
+
| food/apple.html | fruit | small |
|
22
|
+
| food/banana.html | fruit | small |
|
23
|
+
| food/cranberry.html | fruit | small |
|
24
|
+
| food/peanuts.html | nuts | small |
|
25
|
+
| food/walnuts.html | nuts | med |
|
26
|
+
When I query the path "food"
|
27
|
+
And I filter the query with:
|
28
|
+
"""
|
29
|
+
{ "type" : "nuts" }
|
30
|
+
"""
|
31
|
+
And I filter the query with:
|
32
|
+
"""
|
33
|
+
{ "size" : "med" }
|
34
|
+
"""
|
35
|
+
Then this query returns the ordered results "food/walnuts.html"
|
36
|
+
|
37
|
+
Scenario: Query with filter by 2 of the same metadata attributes
|
38
|
+
Given some files with values:
|
39
|
+
| file | type | size |
|
40
|
+
| food/cranberry.html | fruit | med |
|
41
|
+
| food/peanuts.html | nuts | small |
|
42
|
+
| food/walnuts.html | nuts | med |
|
43
|
+
When I query the path "food"
|
44
|
+
And I filter the query with:
|
45
|
+
"""
|
46
|
+
{ "size" : "med" }
|
47
|
+
"""
|
48
|
+
And I filter the query with:
|
49
|
+
"""
|
50
|
+
{ "type" : "nuts" }
|
51
|
+
"""
|
52
|
+
And I filter the query with:
|
53
|
+
"""
|
54
|
+
{ "type" : {"$exists" : true } }
|
55
|
+
"""
|
56
|
+
Then this query returns the ordered results "food/walnuts.html"
|
@@ -0,0 +1,72 @@
|
|
1
|
+
Feature: Query filter by file attributes
|
2
|
+
I want to filter a query so I can better arrange my content for optimized user experience.
|
3
|
+
|
4
|
+
Scenario: Query with filter by extension
|
5
|
+
Given some files with values:
|
6
|
+
| file |
|
7
|
+
| food/apple.html |
|
8
|
+
| food/banana.md |
|
9
|
+
| food/cranberry.html |
|
10
|
+
| food/peanuts.md |
|
11
|
+
When I query the path "food"
|
12
|
+
And I filter the query with:
|
13
|
+
"""
|
14
|
+
{ "$ext" : ".md" }
|
15
|
+
"""
|
16
|
+
Then this query returns the ordered results "food/banana.md, food/peanuts.md"
|
17
|
+
|
18
|
+
Scenario: Query with filter by filename
|
19
|
+
Given some files with values:
|
20
|
+
| file |
|
21
|
+
| food/apple.html |
|
22
|
+
| food/banana.md |
|
23
|
+
| food/cranberry.html |
|
24
|
+
| food/peanuts.md |
|
25
|
+
When I query the path "food"
|
26
|
+
And I filter the query with:
|
27
|
+
"""
|
28
|
+
{ "$filename" : "food/peanuts" }
|
29
|
+
"""
|
30
|
+
Then this query returns the ordered results "food/peanuts.md"
|
31
|
+
|
32
|
+
Scenario: Query with filter by filename nested
|
33
|
+
Given some files with values:
|
34
|
+
| file |
|
35
|
+
| food/fruit/apple.html |
|
36
|
+
| food/fruit/banana.md |
|
37
|
+
| food/banana.html |
|
38
|
+
| food/peanuts.md |
|
39
|
+
When I query the path_all "food"
|
40
|
+
And I filter the query with:
|
41
|
+
"""
|
42
|
+
{ "$filename" : "food/fruit/banana" }
|
43
|
+
"""
|
44
|
+
Then this query returns the ordered results "food/fruit/banana.md"
|
45
|
+
|
46
|
+
Scenario: Query with filter by directories
|
47
|
+
Given some files with values:
|
48
|
+
| file |
|
49
|
+
| food/fruit/apple.html |
|
50
|
+
| food/fruit/banana.md |
|
51
|
+
| food/cranberry.html |
|
52
|
+
| food/peanuts.md |
|
53
|
+
When I query the path_all "food"
|
54
|
+
And I filter the query with:
|
55
|
+
"""
|
56
|
+
{ "$directories" : "fruit" }
|
57
|
+
"""
|
58
|
+
Then this query returns the ordered results "food/fruit/apple.html, food/fruit/banana.md"
|
59
|
+
|
60
|
+
Scenario: Query with filter by shortname
|
61
|
+
Given some files with values:
|
62
|
+
| file |
|
63
|
+
| food/fruit/apple.html |
|
64
|
+
| food/fruit/banana.md |
|
65
|
+
| food/banana.html |
|
66
|
+
| food/peanuts.md |
|
67
|
+
When I query the path_all "food"
|
68
|
+
And I filter the query with:
|
69
|
+
"""
|
70
|
+
{ "$shortname" : "banana" }
|
71
|
+
"""
|
72
|
+
Then this query returns the ordered results "food/banana.html food/fruit/banana.md"
|
data/lib/silly.rb
ADDED
@@ -0,0 +1,174 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
Encoding.default_internal = 'UTF-8'
|
3
|
+
|
4
|
+
require 'json'
|
5
|
+
require 'time'
|
6
|
+
require 'fileutils'
|
7
|
+
require 'delegate'
|
8
|
+
require 'observer'
|
9
|
+
require 'set'
|
10
|
+
|
11
|
+
FileUtils.cd(path = File.join(File.dirname(__FILE__), 'silly')) do
|
12
|
+
Dir[File.join('**', '*.rb')].each { |f| require File.join(path, f) }
|
13
|
+
end
|
14
|
+
|
15
|
+
module Silly
|
16
|
+
FileSeparator = File::ALT_SEPARATOR ?
|
17
|
+
%r{#{ File::SEPARATOR }|#{ File::ALT_SEPARATOR }} :
|
18
|
+
File::SEPARATOR
|
19
|
+
|
20
|
+
class Query
|
21
|
+
include Enumerable
|
22
|
+
attr_accessor :paths
|
23
|
+
|
24
|
+
def initialize
|
25
|
+
@criteria = {
|
26
|
+
"path" => nil,
|
27
|
+
"sort" => ["id", "asc"],
|
28
|
+
"where" => [],
|
29
|
+
}
|
30
|
+
@paths = Set.new
|
31
|
+
end
|
32
|
+
|
33
|
+
def append_path(path)
|
34
|
+
@paths << path
|
35
|
+
end
|
36
|
+
|
37
|
+
BlackList = ["", "~", "/"]
|
38
|
+
def path(path)
|
39
|
+
@criteria["path"] = BlackList.include?(path.to_s) ? "*" : File.join(path, "*")
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
def path_all(path)
|
44
|
+
@criteria["path"] = File.join(path, "**", "**")
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
def sort(conditions=[])
|
49
|
+
@criteria["sort"] = conditions
|
50
|
+
self
|
51
|
+
end
|
52
|
+
|
53
|
+
def where(conditions)
|
54
|
+
@criteria["where"] << conditions
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
def each
|
59
|
+
block_given? ?
|
60
|
+
execute.each { |a| yield(a) } :
|
61
|
+
execute
|
62
|
+
end
|
63
|
+
|
64
|
+
def execute
|
65
|
+
@criteria["path"] ||= "*"
|
66
|
+
puts "EXECUTE:\n #{ @criteria.pretty_inspect }"
|
67
|
+
data = files(@criteria["path"])
|
68
|
+
|
69
|
+
unless @criteria["where"].empty?
|
70
|
+
data = data.keep_if { |id, pointer| filter_function(pointer) }
|
71
|
+
end
|
72
|
+
|
73
|
+
data = data.values.sort { |a,b| sorting_function(a,b) }
|
74
|
+
|
75
|
+
Silly::Collection.new(data)
|
76
|
+
end
|
77
|
+
|
78
|
+
def list
|
79
|
+
results = Set.new
|
80
|
+
|
81
|
+
paths.each do |path|
|
82
|
+
FileUtils.cd(path) {
|
83
|
+
results += Dir['*'].select { |x| File.directory?(x) }
|
84
|
+
}
|
85
|
+
end
|
86
|
+
|
87
|
+
results.to_a
|
88
|
+
end
|
89
|
+
|
90
|
+
def inspect
|
91
|
+
"#{ self.class.name }\n criteria:\n #{ @criteria.inspect }"
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
# Collect all files for the given @collection["path"].
|
97
|
+
# Each item can have 3 file references, one per each cascade level.
|
98
|
+
# The file hashes are collected in order so they will overwrite eachother.
|
99
|
+
# but references to all found items on the cascade are recorded.
|
100
|
+
# @return[Hash] dictionary of Items.
|
101
|
+
def files(glob)
|
102
|
+
dict = {}
|
103
|
+
|
104
|
+
paths.each do |path|
|
105
|
+
FileUtils.cd(path) {
|
106
|
+
Dir[glob].each { |id|
|
107
|
+
next unless File.exist?(id) && FileTest.file?(id)
|
108
|
+
|
109
|
+
filename = id.gsub(Regexp.new("#{ File.extname(id) }$"), '')
|
110
|
+
|
111
|
+
if dict[filename]
|
112
|
+
dict[filename]["cascade"] << File.realpath(id)
|
113
|
+
else
|
114
|
+
dict[filename] = Silly::Item.new({
|
115
|
+
"id" => id,
|
116
|
+
"cascade" => [File.realpath(id)]
|
117
|
+
})
|
118
|
+
end
|
119
|
+
}
|
120
|
+
}
|
121
|
+
end
|
122
|
+
|
123
|
+
dict
|
124
|
+
end
|
125
|
+
|
126
|
+
FileAttributes = %{ id filename shortname directories ext }
|
127
|
+
|
128
|
+
def filter_function(item)
|
129
|
+
@criteria["where"].each do |condition|
|
130
|
+
condition.each do |attribute_name, value|
|
131
|
+
attribute_name = attribute_name.to_s
|
132
|
+
attribute = attribute_name[0] == "$" ?
|
133
|
+
item.__send__(attribute_name[1, attribute_name.size]) :
|
134
|
+
item.data[attribute_name]
|
135
|
+
|
136
|
+
valid = Silly::QueryOperators.execute(attribute, value)
|
137
|
+
|
138
|
+
return false unless valid
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def sorting_function(a, b)
|
144
|
+
attribute = @criteria["sort"][0].to_s
|
145
|
+
direction = @criteria["sort"][1].to_s
|
146
|
+
|
147
|
+
# Optmization to omit parsing internal metadata when unecessary.
|
148
|
+
if FileAttributes.include?(attribute)
|
149
|
+
this_data = a.__send__(attribute)
|
150
|
+
other_data = b.__send__(attribute)
|
151
|
+
else
|
152
|
+
this_data = a.data[attribute]
|
153
|
+
other_data = b.data[attribute]
|
154
|
+
end
|
155
|
+
|
156
|
+
if attribute == "date"
|
157
|
+
if this_data.nil? || other_data.nil?
|
158
|
+
raise(
|
159
|
+
"ArgumentError:" +
|
160
|
+
" The query is sorting on 'date'" +
|
161
|
+
" but '#{ this_data['id'] }' or '#{ other_data['id'] }' has no parseable date in its metadata." +
|
162
|
+
" Add date: 'YYYY-MM-DD' to its metadata."
|
163
|
+
)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
if direction == "asc"
|
168
|
+
this_data <=> other_data
|
169
|
+
else
|
170
|
+
other_data <=> this_data
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Silly
|
2
|
+
class DataModel < SimpleDelegator
|
3
|
+
def process
|
4
|
+
data = {}
|
5
|
+
cascade.each do |path|
|
6
|
+
data = Silly::Utils.deep_merge(data, (Silly::Parse.data_file(path) || {}))
|
7
|
+
end
|
8
|
+
|
9
|
+
{
|
10
|
+
"data" => data,
|
11
|
+
"content" => Silly::Parse.page_file(realpath)
|
12
|
+
}
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/silly/item.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
module Silly
|
2
|
+
class Item
|
3
|
+
include Observable
|
4
|
+
attr_reader :pointer
|
5
|
+
attr_accessor :content, :collection
|
6
|
+
|
7
|
+
def initialize(hash)
|
8
|
+
@pointer = hash
|
9
|
+
end
|
10
|
+
|
11
|
+
def [](key)
|
12
|
+
respond_to?(key) ? __send__(key) : nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def id
|
16
|
+
@pointer["id"]
|
17
|
+
end
|
18
|
+
|
19
|
+
def realpath
|
20
|
+
@pointer["cascade"].last
|
21
|
+
end
|
22
|
+
|
23
|
+
def cascade
|
24
|
+
@pointer["cascade"]
|
25
|
+
end
|
26
|
+
|
27
|
+
def filename
|
28
|
+
@filename ||= id.gsub(Regexp.new("#{ ext }$"), '')
|
29
|
+
end
|
30
|
+
|
31
|
+
def shortname
|
32
|
+
File.basename(id, ext)
|
33
|
+
end
|
34
|
+
|
35
|
+
def directories
|
36
|
+
File.dirname(id).split(Silly::FileSeparator)
|
37
|
+
end
|
38
|
+
|
39
|
+
def model
|
40
|
+
%w{ .json .yaml .yml }.include?(ext) ?
|
41
|
+
"data" :
|
42
|
+
"page"
|
43
|
+
end
|
44
|
+
|
45
|
+
def ext
|
46
|
+
File.extname(id)
|
47
|
+
end
|
48
|
+
|
49
|
+
# @returns[Hash Object] Top page metadata
|
50
|
+
def data
|
51
|
+
@data ||= (_model.process["data"] || {})
|
52
|
+
end
|
53
|
+
|
54
|
+
# @returns[String] Raw page content
|
55
|
+
def content
|
56
|
+
@content ||= (_model.process["content"] || "")
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def _model
|
62
|
+
return @_model if @_model
|
63
|
+
|
64
|
+
klass = if model == "data"
|
65
|
+
Silly::DataModel
|
66
|
+
else
|
67
|
+
Silly::PageModel
|
68
|
+
end
|
69
|
+
|
70
|
+
@_model = klass.new(self)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Silly
|
2
|
+
class PageModel < SimpleDelegator
|
3
|
+
DateMatcher = /^(.+\/)*(\d+-\d+-\d+)-(.*)(\.[^.]+)$/
|
4
|
+
Matcher = /^(.+\/)*(.*)(\.[^.]+)$/
|
5
|
+
|
6
|
+
# Process this file. See #parse_page_file
|
7
|
+
# @return[Hash] the processed data from the file.
|
8
|
+
# ex:
|
9
|
+
# { "content" => "..", "data" => { "key" => "value" } }
|
10
|
+
def process
|
11
|
+
return {} unless file?
|
12
|
+
|
13
|
+
parsed_page = Silly::Parse.page_file(realpath)
|
14
|
+
data = parsed_page['data']
|
15
|
+
|
16
|
+
filename_data = parse_page_filename(id)
|
17
|
+
|
18
|
+
data['pointer'] = pointer.dup
|
19
|
+
data['id'] = id
|
20
|
+
|
21
|
+
data['title'] = data['title'] || filename_data['title']
|
22
|
+
data['date'] ||= filename_data['date']
|
23
|
+
|
24
|
+
# Parse and store date as an object
|
25
|
+
begin
|
26
|
+
data['date'] = Time.parse(data['date']) unless data['date'].nil? || data['date'].is_a?(Time)
|
27
|
+
rescue
|
28
|
+
raise(
|
29
|
+
"ArgumentError: The date '#{data['date']}' specified in '#{ id }' is unparsable."
|
30
|
+
)
|
31
|
+
data['date'] = nil
|
32
|
+
end
|
33
|
+
|
34
|
+
parsed_page['data'] = data
|
35
|
+
|
36
|
+
parsed_page
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
# Is the item backed by a physical file in the filesystem?
|
42
|
+
# @return[Boolean]
|
43
|
+
def file?
|
44
|
+
!!realpath
|
45
|
+
end
|
46
|
+
|
47
|
+
def parse_page_filename(filename)
|
48
|
+
data = *filename.match(DateMatcher)
|
49
|
+
data = *filename.match(Matcher) if data.empty?
|
50
|
+
return {} if data.empty?
|
51
|
+
|
52
|
+
if filename =~ DateMatcher
|
53
|
+
{
|
54
|
+
"path" => data[1],
|
55
|
+
"date" => data[2],
|
56
|
+
"slug" => data[3],
|
57
|
+
"title" => to_title(data[3]),
|
58
|
+
"extension" => data[4]
|
59
|
+
}
|
60
|
+
else
|
61
|
+
{
|
62
|
+
"path" => data[1],
|
63
|
+
"slug" => data[2],
|
64
|
+
"title" => to_title(data[2]),
|
65
|
+
"extension" => data[3]
|
66
|
+
}
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# my-post-title ===> My Post Title
|
71
|
+
def to_title(file_slug)
|
72
|
+
if file_slug == 'index' && !id.index('/').nil?
|
73
|
+
file_slug = id.split('/')[-2]
|
74
|
+
end
|
75
|
+
|
76
|
+
file_slug
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
data/lib/silly/parse.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
module Silly
|
2
|
+
module Parse
|
3
|
+
TopYAMLregex = /^(---\s*\n.*?\n?)^(---\s*$\n?)/m
|
4
|
+
TopJSONregex = /^({\s*\n.*?\n?)^(}\s*$\n?)/m
|
5
|
+
|
6
|
+
# Primary method to parse the file as a page-like object.
|
7
|
+
# File API is currently defines:
|
8
|
+
# 1. Top meta-data
|
9
|
+
# 2. Page Body
|
10
|
+
#
|
11
|
+
# @returns[Hash Object] processed top meta-data, raw (unconverted) content body
|
12
|
+
def self.page_file(filepath)
|
13
|
+
result = {}
|
14
|
+
front_matter = nil
|
15
|
+
format = nil
|
16
|
+
page = File.open(filepath, 'r:UTF-8') { |f| f.read }
|
17
|
+
first_line = page.lines.first.to_s
|
18
|
+
|
19
|
+
begin
|
20
|
+
if (first_line.strip == '---')
|
21
|
+
front_matter = page.match(TopYAMLregex)
|
22
|
+
format = 'yaml'
|
23
|
+
elsif (first_line.strip == '{')
|
24
|
+
front_matter = page.match(TopJSONregex)
|
25
|
+
format = 'json'
|
26
|
+
end
|
27
|
+
rescue => e
|
28
|
+
raise "Error trying to read meta-data from #{ filepath }.
|
29
|
+
It's probably a non text-based file like an image.
|
30
|
+
Please remove it or omit it from your query. Error details: #{ e }"
|
31
|
+
end
|
32
|
+
|
33
|
+
if format == 'yaml'
|
34
|
+
data = yaml_for_pages(front_matter, filepath)
|
35
|
+
result["content"] = page.gsub(TopYAMLregex, '')
|
36
|
+
else
|
37
|
+
data = json_for_pages(front_matter, filepath)
|
38
|
+
result["content"] = page.gsub(TopJSONregex, '')
|
39
|
+
end
|
40
|
+
|
41
|
+
result["data"] = data
|
42
|
+
result
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.data_file(*args)
|
46
|
+
filepath = File.__send__(:join, args)
|
47
|
+
if File.extname(filepath).to_s.empty?
|
48
|
+
path = nil
|
49
|
+
["#{ filepath }.json", "#{ filepath }.yml", "#{ filepath }.yaml"].each do |result|
|
50
|
+
filepath = path = result and break if File.exist?(result)
|
51
|
+
end
|
52
|
+
|
53
|
+
return nil unless path
|
54
|
+
end
|
55
|
+
|
56
|
+
file = File.open(filepath, 'r:UTF-8') { |f| f.read }
|
57
|
+
|
58
|
+
File.extname(filepath) == ".json" ? json(file) : yaml(file)
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.yaml(file)
|
62
|
+
YAML.load(file) || {}
|
63
|
+
rescue Psych::SyntaxError => e
|
64
|
+
raise("ERROR in #{filepath}: #{e.message}")
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.json(file)
|
69
|
+
JSON.load(file) || {}
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.yaml_for_pages(front_matter, filepath)
|
73
|
+
return {} unless front_matter
|
74
|
+
YAML.load(front_matter[0].gsub(/---\n/, "")) || {}
|
75
|
+
rescue Psych::SyntaxError => e
|
76
|
+
raise("Psych::SyntaxError while parsing top YAML Metadata in #{ filepath }\n" +
|
77
|
+
"#{ e.message }\n" +
|
78
|
+
"Try validating the YAML metadata using http://yamllint.com"
|
79
|
+
)
|
80
|
+
nil
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.json_for_pages(front_matter, filepath)
|
84
|
+
return {} unless front_matter
|
85
|
+
JSON.load(front_matter[0]) || {}
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Silly
|
2
|
+
module QueryOperators
|
3
|
+
def self.execute(attribute, value)
|
4
|
+
if value.is_a?(Hash)
|
5
|
+
type, value = value.to_a.first
|
6
|
+
else
|
7
|
+
type = "$equals"
|
8
|
+
value = value.is_a?(Symbol) ? value.to_s : value
|
9
|
+
end
|
10
|
+
|
11
|
+
command = type[1, type.size]
|
12
|
+
|
13
|
+
__send__(command, attribute, value)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.exists(attribute, value)
|
17
|
+
!!attribute == !!value
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.equals(attribute, value)
|
21
|
+
case attribute
|
22
|
+
when Array
|
23
|
+
attribute.include?(value)
|
24
|
+
else
|
25
|
+
attribute == value
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.ne(attribute, value)
|
30
|
+
case attribute
|
31
|
+
when Array
|
32
|
+
!attribute.include?(value)
|
33
|
+
else
|
34
|
+
attribute != value
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.exclude(attribute, value)
|
39
|
+
case attribute
|
40
|
+
when Array
|
41
|
+
attribute.each{ |a| return false if (a =~ value) }
|
42
|
+
else
|
43
|
+
!(attribute =~ value)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/silly/utils.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
module Silly
|
2
|
+
module Utils
|
3
|
+
# Merges hash with another hash, recursively.
|
4
|
+
#
|
5
|
+
# Adapted from Jekyll which got it from some gem whose link is now broken.
|
6
|
+
# Thanks to whoever made it.
|
7
|
+
def self.deep_merge(hash1, hash2)
|
8
|
+
target = hash1.dup
|
9
|
+
|
10
|
+
hash2.keys.each do |key|
|
11
|
+
if hash2[key].is_a?(Hash) && hash1[key].is_a?(Hash)
|
12
|
+
target[key] = deep_merge(target[key], hash2[key])
|
13
|
+
next
|
14
|
+
end
|
15
|
+
|
16
|
+
target[key] = hash2[key]
|
17
|
+
end
|
18
|
+
|
19
|
+
target
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/silly.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
$LOAD_PATH.unshift 'lib'
|
2
|
+
require 'silly/version'
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "silly"
|
6
|
+
s.version = Silly::Version
|
7
|
+
s.date = Time.now.strftime('%Y-%m-%d')
|
8
|
+
s.license = "http://www.opensource.org/licenses/MIT"
|
9
|
+
s.summary = 'Silly is a filesystem based Object Document Mapper.'
|
10
|
+
s.homepage = "http://github.com/ruhoh/silly"
|
11
|
+
s.email = "plusjade@gmail.com"
|
12
|
+
s.authors = ['Jade Dominguez']
|
13
|
+
s.description = 'Silly is an ODM for parsing and querying a directory like you would a database -- useful for static websites.'
|
14
|
+
|
15
|
+
|
16
|
+
s.add_development_dependency 'cucumber'
|
17
|
+
s.add_development_dependency 'capybara'
|
18
|
+
s.add_development_dependency 'rspec'
|
19
|
+
|
20
|
+
s.files = `git ls-files`.
|
21
|
+
split("\n").
|
22
|
+
sort.
|
23
|
+
reject { |file| file =~ /^(\.|rdoc|pkg|coverage)/ }
|
24
|
+
end
|
metadata
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: silly
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jade Dominguez
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-12-30 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: cucumber
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: capybara
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rspec
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
description: Silly is an ODM for parsing and querying a directory like you would a
|
63
|
+
database -- useful for static websites.
|
64
|
+
email: plusjade@gmail.com
|
65
|
+
executables: []
|
66
|
+
extensions: []
|
67
|
+
extra_rdoc_files: []
|
68
|
+
files:
|
69
|
+
- Gemfile
|
70
|
+
- README.md
|
71
|
+
- Rakefile
|
72
|
+
- features/cascade.feature
|
73
|
+
- features/data_file.feature
|
74
|
+
- features/path.feature
|
75
|
+
- features/sort.feature
|
76
|
+
- features/step_defs.rb
|
77
|
+
- features/support/env.rb
|
78
|
+
- features/support/helpers.rb
|
79
|
+
- features/where.feature
|
80
|
+
- features/where_file.feature
|
81
|
+
- lib/silly.rb
|
82
|
+
- lib/silly/base_model.rb
|
83
|
+
- lib/silly/collection.rb
|
84
|
+
- lib/silly/data_model.rb
|
85
|
+
- lib/silly/item.rb
|
86
|
+
- lib/silly/page_model.rb
|
87
|
+
- lib/silly/parse.rb
|
88
|
+
- lib/silly/query_operators.rb
|
89
|
+
- lib/silly/utils.rb
|
90
|
+
- lib/silly/version.rb
|
91
|
+
- silly.gemspec
|
92
|
+
homepage: http://github.com/ruhoh/silly
|
93
|
+
licenses:
|
94
|
+
- http://www.opensource.org/licenses/MIT
|
95
|
+
post_install_message:
|
96
|
+
rdoc_options: []
|
97
|
+
require_paths:
|
98
|
+
- lib
|
99
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
100
|
+
none: false
|
101
|
+
requirements:
|
102
|
+
- - ! '>='
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '0'
|
105
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
106
|
+
none: false
|
107
|
+
requirements:
|
108
|
+
- - ! '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
requirements: []
|
112
|
+
rubyforge_project:
|
113
|
+
rubygems_version: 1.8.24
|
114
|
+
signing_key:
|
115
|
+
specification_version: 3
|
116
|
+
summary: Silly is a filesystem based Object Document Mapper.
|
117
|
+
test_files: []
|