trundle 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3f26381e6daf38ba9ec6a3e756ce71e4cffba2fc
4
+ data.tar.gz: f9a7f0c9858604929a667f6f1cd13e3f5be21542
5
+ SHA512:
6
+ metadata.gz: 2db5e7d5058d0b4e380afe0d9931b1d067ee56c145349c5c4246dc82acf2e575bfceea6103ab1fe8e7a7faddeb5ac4c10e756c7fcdbbd3816f281a0211eb164b
7
+ data.tar.gz: 34973269859c359dd0a4ead77bda5b940f19a264e1d3827b6b9ace03906ad1cbeae87e3a0c471e0ab5b39994f86a89118b2cfaa6ed18090d8e39d76a249d16e0
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.2.0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in trundle.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Andy Pearson
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,170 @@
1
+ # trundle
2
+
3
+ A gem for reading and writing [TextBundle](http://textbundle.org/) files.
4
+
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'trundle'
12
+ ```
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install trundle
21
+
22
+
23
+ ## Usage
24
+
25
+ ### Reading an existing TextBundle
26
+
27
+ Imagine you've got a TextBundle file saved at `~/example-bundle.textbundle`
28
+
29
+ You can open that very easily with just:
30
+
31
+ ```ruby
32
+ example = Trundle.open('~/example-bundle.textbundle')
33
+ ```
34
+
35
+ That'll give you an instance of a `Trundle::TextBundle` and you'll be able to read out the text content of the bundle using:
36
+
37
+ ```ruby
38
+ example.text
39
+ ```
40
+
41
+ You can also access all of the core attributes outlined in the specification:
42
+
43
+ ```ruby
44
+ example.creator_identifier
45
+ example.creator_url
46
+ example.source_url
47
+ example.type
48
+ example.version
49
+ ```
50
+
51
+ As well as finding out if this TextBundle is transient or not:
52
+
53
+ ```ruby
54
+ example.transient?
55
+ ```
56
+
57
+ ### Writing a TextBundle
58
+
59
+ Now imagine you'd like to create a new TextBundle, you can do that by opening a file where one does not already exist (just as you usually do in Ruby)
60
+
61
+ ```ruby
62
+ my_example = Trundle.open('~/my-example-bundle.textbundle')
63
+ ```
64
+
65
+ Setting the text content of this bundle is as easy as:
66
+
67
+ ```ruby
68
+ my_example.text = 'This is my bundle!'
69
+ ```
70
+
71
+ And you can set the values in a similar way:
72
+
73
+ ```ruby
74
+ example.creator_identifier = 'com.my-app'
75
+ example.creator_url = 'http://my-app.com'
76
+ example.source_url = 'http://my-app.com'
77
+ example.type = 'net.daringfireball.markdown'
78
+ example.version = 2
79
+ example.transient = false
80
+ ```
81
+
82
+ Once you're happy with the state of your new TextBundle you can save it to the filesystem by calling:
83
+
84
+ ```ruby
85
+ example.close
86
+ ```
87
+
88
+ Passing a block to `Trundle.open` will set values and write the bundle in one fell swoop:
89
+
90
+ ```ruby
91
+ Trundle.open('~/my-example-bundle.textbundle') do |tb|
92
+ tb.text = 'This is my bundle!'
93
+ tb.source_url = 'http://my-app.com'
94
+ end
95
+ ```
96
+
97
+
98
+ ### Setting up default values
99
+
100
+ Setting up your new TextBundles over and over again can get pretty boring. Instead, you can set up defaults that'll apply to any TextBundle that you create:
101
+
102
+ ```ruby
103
+ Trundle.configure do |config|
104
+ config.creator_identifier = 'com.my-app'
105
+ config.creator_url = 'http://my-app.com'
106
+ config.source_url = 'http://my-app.com'
107
+ config.type = 'net.daringfireball.markdown'
108
+ config.version = 2
109
+ config.transient = false
110
+ end
111
+ ```
112
+
113
+ Out of the box, trundle is setup with the following defaults:
114
+
115
+ ```ruby
116
+ Trundle.configure do |config|
117
+ config.version = 2
118
+ config.transient = false
119
+ end
120
+ ```
121
+
122
+
123
+ ### Working with namespaces
124
+
125
+ Applications can add custom meta data to any TextBundle by including it under a unique application key. In Trundle, this is called a namespace.
126
+
127
+ You need to tell trundle about the namespaces that you'd like to work with. You can do that in the config:
128
+
129
+ ```ruby
130
+ Trundle.configure do |config|
131
+ config.namespaces do
132
+ my_app 'com.my-app'
133
+ end
134
+ end
135
+ ```
136
+
137
+ The bit on the left defines the name of the accessor method and the bit on the right is the application key that'll be read and written. You can setup as many namespaces as you need.
138
+
139
+ Once trundle knows about your namespaces, you can read custom values from them:
140
+
141
+ ```ruby
142
+ existing_document = Trundle.open('~/document.textbundle')
143
+ existing_document.my_app.version
144
+ existing_document.my_app.document_type
145
+ ```
146
+
147
+ And also write custom values too:
148
+
149
+ ```ruby
150
+ my_document = Trundle.open('~/my-document.textbundle')
151
+ my_document.my_app.version = 42
152
+ my_document.my_app.document_type = 'Article'
153
+ my_document.close
154
+ ```
155
+
156
+
157
+ ## Next things
158
+
159
+ - TODO: rubocop the code
160
+ - TODO: add support for assets
161
+ - TODO: could the configuration DSL be more succinct?
162
+ - TODO: default values for namespaces?
163
+
164
+ ## Contributing
165
+
166
+ 1. Fork it ( https://github.com/andypearson/trundle/fork )
167
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
168
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
169
+ 4. Push to the branch (`git push origin my-new-feature`)
170
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
@@ -0,0 +1,61 @@
1
+ class Trundle::Config
2
+ attr_accessor(
3
+ :creator_identifier,
4
+ :creator_url,
5
+ :source_url,
6
+ :transient,
7
+ :type,
8
+ :version
9
+ )
10
+
11
+ def initialize
12
+ @version = 2
13
+ @transient = false
14
+ end
15
+
16
+ def namespace?(name)
17
+ namespace_list.include?(name)
18
+ end
19
+
20
+ def namespace_key(name)
21
+ namespace_list.key(name)
22
+ end
23
+
24
+ def namespaces(&block)
25
+ if block_given?
26
+ namespace_list.instance_eval(&block)
27
+ else
28
+ namespace_list.all
29
+ end
30
+ end
31
+
32
+ def transient=(value)
33
+ raise ArgumentError, 'transient must be a boolean' unless !!value == value
34
+ @transient = value
35
+ end
36
+
37
+ def to_h
38
+ hash = {}
39
+ keys.each do |key|
40
+ value = send(key)
41
+ hash[Trundle::Key.new(key).camelize] = value if !value.nil?
42
+ end
43
+ hash
44
+ end
45
+
46
+ private
47
+ def keys
48
+ [
49
+ :creator_identifier,
50
+ :creator_url,
51
+ :source_url,
52
+ :transient,
53
+ :type,
54
+ :version
55
+ ]
56
+ end
57
+
58
+ def namespace_list
59
+ @namespace_list ||= Trundle::NamespaceList.new
60
+ end
61
+ end
@@ -0,0 +1,15 @@
1
+ module Trundle::InfoAccessors
2
+ def info_accessors(*args)
3
+ args.each do |arg|
4
+ key = Trundle::Key.new(arg).camelize
5
+
6
+ define_method(arg) do
7
+ info[key]
8
+ end
9
+
10
+ define_method("#{arg}=") do |value|
11
+ info[key] = value
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,23 @@
1
+ require 'json'
2
+
3
+ class Trundle::InfoStore
4
+ def initialize(path)
5
+ @path = path
6
+ end
7
+
8
+ def content=(value)
9
+ @content = value
10
+ end
11
+
12
+ def content
13
+ @content ||= JSON.parse(File.read(@path))
14
+ rescue Errno::ENOENT
15
+ @content = {}
16
+ end
17
+
18
+ def write
19
+ File.open(@path, 'w') do |file|
20
+ file.write(JSON.pretty_generate(content))
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,15 @@
1
+ require 'active_support/inflector'
2
+
3
+ class Trundle::Key
4
+ def initialize(key)
5
+ @key = key.to_s
6
+ end
7
+
8
+ def camelize
9
+ ActiveSupport::Inflector.camelize(@key, false).gsub(/url$/i, 'URL')
10
+ end
11
+
12
+ def underscore
13
+ ActiveSupport::Inflector.underscore(@key)
14
+ end
15
+ end
@@ -0,0 +1,21 @@
1
+ class Trundle::NamespaceList
2
+ def initialize
3
+ @namespaces = {}
4
+ end
5
+
6
+ def include?(name)
7
+ @namespaces.has_key? name.to_sym
8
+ end
9
+
10
+ def key(name)
11
+ @namespaces[name.to_sym]
12
+ end
13
+
14
+ def all
15
+ @namespaces
16
+ end
17
+
18
+ def method_missing(name, *args, &block)
19
+ @namespaces[name.to_sym] = args[0]
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ class Trundle::NamespacedAttributes < OpenStruct
2
+ def initialize(attributes = {})
3
+ super(normalize_attributes(attributes))
4
+ end
5
+
6
+ def to_h
7
+ attrs = {}
8
+ super.to_h.each do |key, value|
9
+ attrs[Trundle::Key.new(key).camelize] = value
10
+ end
11
+ attrs
12
+ end
13
+
14
+ private
15
+ def normalize_attributes(attributes)
16
+ normalized_attributes = {}
17
+ attributes.each do |key, value|
18
+ key = Trundle::Key.new(key).underscore
19
+ normalized_attributes[key] = value
20
+ end
21
+ normalized_attributes
22
+ end
23
+ end
@@ -0,0 +1,95 @@
1
+ class Trundle::TextBundle
2
+ extend Trundle::InfoAccessors
3
+
4
+ info_accessors(
5
+ :creator_identifier,
6
+ :creator_url,
7
+ :source_url,
8
+ :transient,
9
+ :type,
10
+ :version
11
+ )
12
+
13
+ def initialize(path)
14
+ @path = path
15
+ @info_store = Trundle::InfoStore.new(File.join(@path, 'info.json'))
16
+ @text_store = Trundle::TextStore.new(File.join(@path, 'text.markdown'))
17
+
18
+ @info_store.content = Trundle.config.to_h unless exist?
19
+
20
+ if block_given?
21
+ yield(self)
22
+ close
23
+ end
24
+ end
25
+
26
+ def exist?
27
+ File.exist?(@path)
28
+ end
29
+
30
+ def text
31
+ @text_store.content
32
+ end
33
+
34
+ def text=(value)
35
+ @text_store.content = value
36
+ end
37
+
38
+ def close
39
+ Dir.mkdir(@path) unless exist?
40
+ write_namespaces
41
+ @info_store.write
42
+ @text_store.write
43
+ end
44
+
45
+ def transient=(value)
46
+ raise ArgumentError, 'transient must be a boolean' unless !!value == value
47
+ info['transient'] = value
48
+ end
49
+
50
+ def transient?
51
+ !!info['transient']
52
+ end
53
+
54
+ def method_missing(name, *args, &block)
55
+ if Trundle.config.namespace?(name)
56
+ namespaced_attributes_for(name)
57
+ else
58
+ raise Trundle::NamespaceNotDefined, namespace_not_defined_message(name)
59
+ end
60
+ end
61
+
62
+ private
63
+ def info
64
+ @info_store.content
65
+ end
66
+
67
+ def namespaced_attributes_for(name)
68
+ var = "@#{name}"
69
+ if !instance_variable_defined?(var)
70
+ attributes = info[Trundle.config.namespace_key(name)] || {}
71
+ instance_variable_set(var, Trundle::NamespacedAttributes.new(attributes))
72
+ end
73
+ instance_variable_get(var)
74
+ end
75
+
76
+ def write_namespaces
77
+ Trundle.config.namespaces.each do |name, key|
78
+ info[key] = namespaced_attributes_for(name).to_h
79
+ end
80
+ end
81
+
82
+ def namespace_not_defined_message(name)
83
+ <<-STRING.gsub(/^ {6}/, '').strip
84
+ The namespace "#{name}" is not defined!
85
+
86
+ Add it to your config using:
87
+
88
+ Trundle.configure do |config|
89
+ config.namespaces do
90
+ #{name} 'unique.namespace.key'
91
+ end
92
+ end
93
+ STRING
94
+ end
95
+ end
@@ -0,0 +1,19 @@
1
+ class Trundle::TextStore
2
+ attr_writer :content
3
+
4
+ def initialize(path)
5
+ @path = path
6
+ end
7
+
8
+ def content
9
+ @content ||= File.read(@path)
10
+ rescue
11
+ @content = ''
12
+ end
13
+
14
+ def write
15
+ File.open(@path, 'w') do |file|
16
+ file.write(content)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,3 @@
1
+ class Trundle
2
+ VERSION = '0.1.0'
3
+ end
data/lib/trundle.rb ADDED
@@ -0,0 +1,31 @@
1
+ require 'trundle/version'
2
+ require 'trundle/info_accessors'
3
+
4
+ require 'trundle/config'
5
+ require 'trundle/info_store'
6
+ require 'trundle/key'
7
+ require 'trundle/namespace_list'
8
+ require 'trundle/namespaced_attributes'
9
+ require 'trundle/text_bundle'
10
+ require 'trundle/text_store'
11
+
12
+ class Trundle
13
+
14
+ class NamespaceNotDefined < StandardError; end
15
+
16
+ def self.open(path)
17
+ if block_given?
18
+ TextBundle.new(path, &Proc.new)
19
+ else
20
+ TextBundle.new(path)
21
+ end
22
+ end
23
+
24
+ def self.configure
25
+ yield config
26
+ end
27
+
28
+ def self.config
29
+ @config ||= Config.new
30
+ end
31
+ end
@@ -0,0 +1,113 @@
1
+ RSpec.describe Trundle::Config do
2
+ let(:config) { described_class.new }
3
+
4
+ it 'has a default version' do
5
+ expect(config.version).to eq(2)
6
+ end
7
+
8
+ it 'is not transient by default' do
9
+ expect(config.transient).to be false
10
+ end
11
+
12
+ describe 'version' do
13
+ let(:version) { 1 }
14
+
15
+ before do
16
+ config.version = version
17
+ end
18
+
19
+ it 'is set' do
20
+ expect(config.version).to eq(version)
21
+ end
22
+
23
+ it 'is included in the hash' do
24
+ expect(config.to_h).to include('version' => version)
25
+ end
26
+ end
27
+
28
+ describe 'type' do
29
+ let(:type) { 'net.daringfireball.markdown' }
30
+
31
+ before do
32
+ config.type = type
33
+ end
34
+
35
+ it 'is set' do
36
+ expect(config.type).to eq(type)
37
+ end
38
+
39
+ it 'is included in the hash' do
40
+ expect(config.to_h).to include('type' => type)
41
+ end
42
+ end
43
+
44
+ describe 'transient' do
45
+ let(:transient) { true }
46
+
47
+ before do
48
+ config.transient = transient
49
+ end
50
+
51
+ it 'is set' do
52
+ expect(config.transient).to eq(transient)
53
+ end
54
+
55
+ it 'is included in the hash' do
56
+ expect(config.to_h).to include('transient' => transient)
57
+ end
58
+
59
+ it 'raises an error when the value is not a boolean' do
60
+ [[], '', nil, {}].each do |value|
61
+ expect{ config.transient = value }.to raise_error(ArgumentError, 'transient must be a boolean')
62
+ end
63
+ end
64
+ end
65
+
66
+ describe 'creator_url' do
67
+ let(:creator_url) { 'file:///Applications/MyApp' }
68
+
69
+ before do
70
+ config.creator_url = creator_url
71
+ end
72
+
73
+ it 'is set' do
74
+ expect(config.creator_url).to eq(creator_url)
75
+ end
76
+
77
+ it 'is included in the hash' do
78
+ expect(config.to_h).to include('creatorURL' => creator_url)
79
+ end
80
+ end
81
+
82
+ describe 'creator_identifier' do
83
+ let(:creator_identifier) { 'com.example.myapp' }
84
+
85
+ before do
86
+ config.creator_identifier = creator_identifier
87
+ end
88
+
89
+ it 'is set' do
90
+ expect(config.creator_identifier).to eq(creator_identifier)
91
+ end
92
+
93
+ it 'is included in the hash' do
94
+ expect(config.to_h).to include('creatorIdentifier' => creator_identifier)
95
+ end
96
+ end
97
+
98
+ describe 'source_url' do
99
+ let(:source_url) { 'file:///Users/johndoe/Documents/mytext.markdown' }
100
+
101
+ before do
102
+ config.source_url = source_url
103
+ end
104
+
105
+ it 'is set' do
106
+ expect(config.source_url).to eq(source_url)
107
+ end
108
+
109
+ it 'is included in the hash' do
110
+ expect(config.to_h).to include('sourceURL' => source_url)
111
+ end
112
+ end
113
+ end