silly 0.0.1
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.
- 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
|
+
[](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: []
|