text_record 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/Rakefile +40 -0
- data/VERSION +1 -0
- data/lib/text_record/attribute.rb +27 -0
- data/lib/text_record/base.rb +65 -0
- data/lib/text_record/configuration.rb +21 -0
- data/lib/text_record/errors.rb +13 -0
- data/lib/text_record.rb +14 -0
- data/readme.markdown +76 -0
- data/spec/app/content/blog_posts/TextRecord/attributes.yml +5 -0
- data/spec/app/content/blog_posts/TextRecord/post.markdown +1 -0
- data/spec/app/models/blog_post.rb +5 -0
- data/spec/base_spec.rb +54 -0
- data/spec/blog_post_spec.rb +84 -0
- data/spec/configuration_spec.rb +0 -0
- data/spec/spec_helper.rb +34 -0
- metadata +74 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
coverage/
|
data/Rakefile
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'spec'
|
3
|
+
require 'spec/rake/spectask'
|
4
|
+
|
5
|
+
desc "Run all specs"
|
6
|
+
Spec::Rake::SpecTask.new('spec') do |t|
|
7
|
+
t.spec_opts = ['--colour --format specdoc --loadby mtime --reverse']
|
8
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
9
|
+
end
|
10
|
+
|
11
|
+
desc "Print specdocs"
|
12
|
+
Spec::Rake::SpecTask.new(:doc) do |t|
|
13
|
+
t.spec_opts = ["--format", "specdoc", "--dry-run"]
|
14
|
+
t.spec_files = FileList['spec/*_spec.rb']
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "Generate RCov code coverage report"
|
18
|
+
Spec::Rake::SpecTask.new('rcov') do |t|
|
19
|
+
t.spec_files = FileList['spec/*_spec.rb']
|
20
|
+
t.rcov = true
|
21
|
+
t.rcov_opts = ['--exclude', '/gems/']
|
22
|
+
end
|
23
|
+
|
24
|
+
task :default => :spec
|
25
|
+
|
26
|
+
begin
|
27
|
+
require 'jeweler'
|
28
|
+
Jeweler::Tasks.new do |gemspec|
|
29
|
+
gemspec.name = "text_record"
|
30
|
+
gemspec.summary = "Models through text files"
|
31
|
+
gemspec.description = "Write your models in text files through YAML/Makdown/TextFile whatever you please. Take the forms out of your code and harness the power of your favorite editor."
|
32
|
+
gemspec.email = "Adman1965@gmaiil.com"
|
33
|
+
gemspec.homepage = "http://github.com/Adman65/text_record"
|
34
|
+
gemspec.authors = ["Adam Hawkins"]
|
35
|
+
end
|
36
|
+
|
37
|
+
Jeweler::GemcutterTasks.new
|
38
|
+
rescue LoadError
|
39
|
+
puts "Jeweler not available. Install it with: gem install jeweler"
|
40
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module TextRecord
|
2
|
+
class Attribute
|
3
|
+
attr_reader :name, :options
|
4
|
+
|
5
|
+
def self.for_options(options)
|
6
|
+
(options[:from].eql?(:yml)) ? YAMLAttribute : FileAttribute
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(name, options)
|
10
|
+
@name = name
|
11
|
+
@options = options
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class YAMLAttribute < Attribute
|
16
|
+
def parse(obj)
|
17
|
+
obj.instance_variable_set("@#{name.to_s.underscore}".to_sym, obj.yml[name.to_s.camelcase])
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class FileAttribute < Attribute
|
22
|
+
def parse(obj)
|
23
|
+
file_contents = IO.read("#{obj.directory}/#{options[:from]}")
|
24
|
+
obj.instance_variable_set("@#{name.to_s.underscore}".to_sym, file_contents)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
module TextRecord
|
5
|
+
class Base
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
attr_reader :slug
|
9
|
+
|
10
|
+
def_delegators :configuration, :content_path, :attribute_file
|
11
|
+
|
12
|
+
# Since attributes are unique to each subclass, have to the object's metaclass since it will be
|
13
|
+
# unique to each subclass
|
14
|
+
|
15
|
+
class << self
|
16
|
+
attr_reader :attributes #aka @attributes in this block
|
17
|
+
|
18
|
+
def attribute(name, options = {})
|
19
|
+
options.assert_valid_keys :from
|
20
|
+
|
21
|
+
@attributes ||= []
|
22
|
+
options[:from] ||= :yml
|
23
|
+
|
24
|
+
self.class_eval do
|
25
|
+
attr_reader name.to_s.underscore.to_sym
|
26
|
+
end
|
27
|
+
|
28
|
+
@attributes << Attribute.for_options(options).new(name, options)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# class methods
|
33
|
+
def self.directory
|
34
|
+
File.join(TextRecord.configuration.content_path, self.name.pluralize.underscore)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Instance Methods
|
38
|
+
def initialize(path)
|
39
|
+
@slug = path
|
40
|
+
|
41
|
+
raise SlugNotFoundError.new("#{directory} could not be found.") unless File.directory?(directory)
|
42
|
+
|
43
|
+
self.class.attributes.each do |attribute| # class attributes, attr_readers to be added to object
|
44
|
+
attribute.parse(self)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def configuration
|
49
|
+
TextRecord.configuration
|
50
|
+
end
|
51
|
+
|
52
|
+
def directory
|
53
|
+
name = self.class.name.underscore.pluralize
|
54
|
+
File.join(content_path, name, slug)
|
55
|
+
end
|
56
|
+
|
57
|
+
def yml_file
|
58
|
+
File.join(directory, attribute_file)
|
59
|
+
end
|
60
|
+
|
61
|
+
def yml
|
62
|
+
@yml ||= YAML.load_file(yml_file)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module TextRecord
|
2
|
+
def self.configuration
|
3
|
+
@configuration ||= Configuration.new
|
4
|
+
end
|
5
|
+
|
6
|
+
|
7
|
+
def self.configure(config=configuration)
|
8
|
+
yield config if block_given?
|
9
|
+
@configuration = config
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
class Configuration
|
14
|
+
attr_accessor :content_path
|
15
|
+
attr_accessor :attribute_file
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
self.attribute_file = "attributes.yml"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module TextRecord
|
2
|
+
# General error class
|
3
|
+
class TextRecordError < StandardError
|
4
|
+
end
|
5
|
+
|
6
|
+
# Raised when the attribute file cannot be found
|
7
|
+
class AttributeFileNotFoundError < TextRecordError
|
8
|
+
end
|
9
|
+
|
10
|
+
# Raised when slug cannot be found
|
11
|
+
class SlugNotFoundError < TextRecordError
|
12
|
+
end
|
13
|
+
end
|
data/lib/text_record.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
begin
|
3
|
+
require 'active_support'
|
4
|
+
rescue
|
5
|
+
require 'active_support'
|
6
|
+
end
|
7
|
+
|
8
|
+
module TextRecord
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'text_record/configuration'
|
12
|
+
require 'text_record/errors'
|
13
|
+
require 'text_record/attribute'
|
14
|
+
require 'text_record/base'
|
data/readme.markdown
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
# TextRecord
|
2
|
+
|
3
|
+
I needed to create a personal website with a lot of hand written content like blog posts, book reviews, and project documentations. Being a lazy programmer, I didn't want to have to write useless admin interfaces for creating all this stuff. Also, you can't get the full editing experience inside a textarea. TextMate totally owns textareas. So I needed an easy way to create massive amounts of text and organize them. Essentially, I wanted a way to treat my website as an open word document that I could save and push to heroku.
|
4
|
+
|
5
|
+
After playing around with the idea and implementation I came up with TextRecord. TextRecord uses patterns from ActiveRecord to model objects in various text files.
|
6
|
+
|
7
|
+
# Example
|
8
|
+
|
9
|
+
## Install TextRecord
|
10
|
+
|
11
|
+
sudo gem install text_record # not gemmed yet
|
12
|
+
|
13
|
+
## Creating a Model
|
14
|
+
|
15
|
+
All model files live in the content directory. Then each model instance has its own folder. The folder name is the model's slug. Inside each slug directory are the files that contain the model's attributes. Here is an exmaple folder structure
|
16
|
+
|
17
|
+
/content
|
18
|
+
/blog_posts
|
19
|
+
/TextRecordTest
|
20
|
+
attributes.yml
|
21
|
+
post.markdown
|
22
|
+
|
23
|
+
Folder names under content should be underscored. Your slug should be something simple either in camelcase or dashed. Think of the slug has an id column in table.
|
24
|
+
|
25
|
+
Most of the time your attribute will come from the attributes.yml. Defining attributes in the yml file is easy. Here is an example attributes.yml:
|
26
|
+
|
27
|
+
Title: Introducting TextRecord
|
28
|
+
|
29
|
+
Introduction: This is a paragraph explaining TextRecord
|
30
|
+
|
31
|
+
Tags: [Ruby, Gems, TextRecord]
|
32
|
+
|
33
|
+
As you can see this is just vanilla YAML. You can use any YAML sturcture: list, hashes, blocks etc. They are translated into their Ruby equivalents using Ruby's YAML libary. Next we define what attributes to load from the file. By defining the attributes to load instead of autoloading everything in file, you can keep some things in 'development' mode. For example, you're working on creating summaries for long posts, but aren't ready to use them in production yet. Now, lets create the Blog Post model.
|
34
|
+
|
35
|
+
# blog_post.rb
|
36
|
+
BlogPost < TextRecord::Base
|
37
|
+
attribute :title
|
38
|
+
attribute :introduction
|
39
|
+
attribute :tags
|
40
|
+
end
|
41
|
+
|
42
|
+
Use the attribute class method to define methods. Attribute names should be underscored. Attribute names in the yaml file should be camel cased. BlogPost correspond to blog_posts under /content. Finally configure TextRecord.
|
43
|
+
|
44
|
+
TextRecord.configure do |config|
|
45
|
+
config.content_path = "/path/to/content"
|
46
|
+
end
|
47
|
+
|
48
|
+
Now you can instantiate your models:
|
49
|
+
|
50
|
+
blog_post = BlogPost.new 'TextRecordText' # argument is a slug
|
51
|
+
#<BlogPost:0x101e1ee00 @tags=["Ruby", "Gems", "TextRecord"], @slug="TextRecord", @introduction="This is a paragraph explaining TextRecord", @title="Introducting TextRecord", @yml={"Title"=>"Introducting TextRecord", "Tags"=>["Ruby", "Gems", "TextRecord"], "Introduction"=>"This is a paragraph explaining TextRecord"}>
|
52
|
+
>> blog_post.title
|
53
|
+
=> "Introducting TextRecord"
|
54
|
+
>> blog_post.slug
|
55
|
+
=> "TextRecord"
|
56
|
+
>> blog_post.tags
|
57
|
+
=> ["Ruby", "Gems", "TextRecord"]
|
58
|
+
|
59
|
+
# Using Other Files
|
60
|
+
|
61
|
+
You can also pull in attributes from other files. For example, storing markdown/textile or formats that require spacing/tabs in yaml files is difficult. The attribute takes on option: :from. Use from to tell TextRecord to load an attribute from another file. For exmaple, the post text is stored in markdown as 'post' from post.markdown. post.markdown is located under a slug directory.
|
62
|
+
|
63
|
+
class BlogPost < TextRecord::Base
|
64
|
+
attribute :post, :from => 'post.markdown'
|
65
|
+
end
|
66
|
+
|
67
|
+
That will load the contents for post.markdown into the post attr_reader
|
68
|
+
|
69
|
+
|
70
|
+
# Limitations
|
71
|
+
|
72
|
+
TextRecord models are **READ ONLY**. All attributes are attr_readers. Why? Because TextRecord is only a way to encapsulate your text files into objects that you can use in your program. If you want to created/edit/delete models, update your slugs/text files.
|
73
|
+
|
74
|
+
# Working On
|
75
|
+
* **ActiveRecord style finders**
|
76
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
This is from the markdown file
|
data/spec/base_spec.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe TextRecord::Base do
|
4
|
+
describe "#attribute" do
|
5
|
+
|
6
|
+
it "should raise an error if the keys are invalid" do
|
7
|
+
lambda {TextRecord::Base.attribute(:name, :adfsadf => "asdfasdfasdf")}.should raise_error
|
8
|
+
end
|
9
|
+
|
10
|
+
describe TextRecord::YAMLAttribute do
|
11
|
+
before(:each) do
|
12
|
+
class AttributeTest < TextRecord::Base
|
13
|
+
attribute :name
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
after(:each) do
|
18
|
+
AttributeTest.reset_attributes!
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should create a new YAML attribute by default" do
|
22
|
+
AttributeTest.attributes.first.should be_a(TextRecord::YAMLAttribute)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should set the @name of a new YAML attribute" do
|
26
|
+
AttributeTest.attributes.first.name.should eql(:name)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe TextRecord::FileAttribute do
|
31
|
+
before(:each) do
|
32
|
+
class AttributeTest < TextRecord::Base
|
33
|
+
attribute :post, :from => 'post.md'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
after(:each) do
|
38
|
+
AttributeTest.reset_attributes!
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should should create a new FILEAttribute when given a :from key" do
|
42
|
+
AttributeTest.attributes.first.should be_a(TextRecord::FileAttribute)
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should set the @name of a new File attribute" do
|
46
|
+
AttributeTest.attributes.first.name.should eql(:post)
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should set the @options for a new File attribute" do
|
50
|
+
AttributeTest.attributes.first.options.should eql({:from => 'post.md'})
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# this spec essentially tests TextRecord::Base since TextRecord::Base is essentially
|
2
|
+
# an abstract class.
|
3
|
+
|
4
|
+
require 'spec_helper'
|
5
|
+
|
6
|
+
describe BlogPost do
|
7
|
+
describe "#directory" do
|
8
|
+
it "should return the TextRecord.configuration's content_path joined with the class name" do
|
9
|
+
BlogPost.directory.should eql(File.join(TextRecord.configuration.content_path, "blog_posts"))
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "#initialize" do
|
14
|
+
it "should raise an error given an incorrect slug" do
|
15
|
+
# tests for TextRecord::SlugNotFound that has the slug in the error description somewhere
|
16
|
+
lambda { BlogPost.new("fakeslug") }.should raise_error(TextRecord::SlugNotFoundError, /fakeslug/)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should set the slug" do
|
20
|
+
BlogPost.new("TextRecord").slug.should eql("TextRecord")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "A Blog Post" do
|
26
|
+
before(:each) do
|
27
|
+
@blog_post = BlogPost.new('TextRecord')
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "attributes from yaml" do
|
31
|
+
it "should have a title attribute" do
|
32
|
+
@blog_post.respond_to?(:title).should be_true
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should make the title a string" do
|
36
|
+
@blog_post.title.should be_a(String)
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should take the title from the file" do
|
40
|
+
@blog_post.title.should eql('Text Record')
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should not have title writer" do
|
44
|
+
@blog_post.respond_to?(:title=).should be_false
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should have a slug" do
|
48
|
+
@blog_post.slug.should eql('TextRecord')
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should have an introduction_text attribute" do
|
52
|
+
@blog_post.respond_to?(:introduction_text).should be_true
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should set the introduction_text from the yaml file" do
|
56
|
+
@blog_post.introduction_text.should eql('This is an introduction paragraph')
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should not have an attribute writer for introduction_text" do
|
60
|
+
@blog_post.respond_to?(:introduction_text=).should be_false
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe "attributes from files" do
|
65
|
+
it "should have a text attribute" do
|
66
|
+
@blog_post.respond_to?(:text).should be_true
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should assign text to the whole file contents" do
|
70
|
+
@blog_post.text.should eql('This is from the markdown file')
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should not have an text attribute writer" do
|
74
|
+
@blog_post.respond_to?(:text=).should be_false
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
describe "#directory" do
|
80
|
+
it "should return the content path plus the slug" do
|
81
|
+
BlogPost.new('TextRecord').directory.should eql(File.join(TextRecord.configuration.content_path, "blog_posts","TextRecord"))
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
File without changes
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'spec'
|
3
|
+
|
4
|
+
LIB_PATH = File.expand_path("#{File.dirname(__FILE__)}/../lib")
|
5
|
+
$: << LIB_PATH
|
6
|
+
|
7
|
+
require 'text_record'
|
8
|
+
|
9
|
+
APP_PATH = File.expand_path("#{File.dirname(__FILE__)}/app")
|
10
|
+
|
11
|
+
TextRecord.configure do |config|
|
12
|
+
config.content_path = "#{APP_PATH}/content"
|
13
|
+
end
|
14
|
+
|
15
|
+
$: << "#{APP_PATH}/models"
|
16
|
+
|
17
|
+
Dir["#{APP_PATH}/models/*.rb"].each do |model|
|
18
|
+
require model
|
19
|
+
end
|
20
|
+
|
21
|
+
Spec::Runner.configure do |config|
|
22
|
+
config.after(:each) do
|
23
|
+
# nothing as of now
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Little monkey patch to reset attributes during testing
|
28
|
+
module TextRecord
|
29
|
+
class Base
|
30
|
+
def self.reset_attributes!
|
31
|
+
@attributes = []
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
metadata
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: text_record
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Adam Hawkins
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-02-04 00:00:00 -08:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Write your models in text files through YAML/Makdown/TextFile whatever you please. Take the forms out of your code and harness the power of your favorite editor.
|
17
|
+
email: Adman1965@gmaiil.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
24
|
+
files:
|
25
|
+
- .gitignore
|
26
|
+
- Rakefile
|
27
|
+
- VERSION
|
28
|
+
- lib/text_record.rb
|
29
|
+
- lib/text_record/attribute.rb
|
30
|
+
- lib/text_record/base.rb
|
31
|
+
- lib/text_record/configuration.rb
|
32
|
+
- lib/text_record/errors.rb
|
33
|
+
- readme.markdown
|
34
|
+
- spec/app/content/blog_posts/TextRecord/attributes.yml
|
35
|
+
- spec/app/content/blog_posts/TextRecord/post.markdown
|
36
|
+
- spec/app/models/blog_post.rb
|
37
|
+
- spec/base_spec.rb
|
38
|
+
- spec/blog_post_spec.rb
|
39
|
+
- spec/configuration_spec.rb
|
40
|
+
- spec/spec_helper.rb
|
41
|
+
has_rdoc: true
|
42
|
+
homepage: http://github.com/Adman65/text_record
|
43
|
+
licenses: []
|
44
|
+
|
45
|
+
post_install_message:
|
46
|
+
rdoc_options:
|
47
|
+
- --charset=UTF-8
|
48
|
+
require_paths:
|
49
|
+
- lib
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: "0"
|
55
|
+
version:
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: "0"
|
61
|
+
version:
|
62
|
+
requirements: []
|
63
|
+
|
64
|
+
rubyforge_project:
|
65
|
+
rubygems_version: 1.3.5
|
66
|
+
signing_key:
|
67
|
+
specification_version: 3
|
68
|
+
summary: Models through text files
|
69
|
+
test_files:
|
70
|
+
- spec/app/models/blog_post.rb
|
71
|
+
- spec/base_spec.rb
|
72
|
+
- spec/blog_post_spec.rb
|
73
|
+
- spec/configuration_spec.rb
|
74
|
+
- spec/spec_helper.rb
|