lazy_doc 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gitignore +18 -0
- data/.rspec +1 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +84 -0
- data/Rakefile +2 -0
- data/lazy_doc.gemspec +22 -0
- data/lib/lazy_doc.rb +5 -0
- data/lib/lazy_doc/attribute_not_found_error.rb +3 -0
- data/lib/lazy_doc/commands.rb +10 -0
- data/lib/lazy_doc/commands/as_class_command.rb +13 -0
- data/lib/lazy_doc/commands/default_value_command.rb +19 -0
- data/lib/lazy_doc/commands/extract_command.rb +15 -0
- data/lib/lazy_doc/commands/finally_command.rb +15 -0
- data/lib/lazy_doc/commands/via_command.rb +21 -0
- data/lib/lazy_doc/dsl.rb +86 -0
- data/lib/lazy_doc/memoizer.rb +12 -0
- data/lib/lazy_doc/version.rb +3 -0
- data/spec/acceptance/basic_behavior_spec.rb +57 -0
- data/spec/acceptance/support/user.json +25 -0
- data/spec/lib/lazy_doc/commands/as_class_command_spec.rb +42 -0
- data/spec/lib/lazy_doc/commands/default_value_command_spec.rb +60 -0
- data/spec/lib/lazy_doc/commands/extract_command_spec.rb +28 -0
- data/spec/lib/lazy_doc/commands/finally_command_spec.rb +30 -0
- data/spec/lib/lazy_doc/commands/via_command_spec.rb +36 -0
- data/spec/lib/lazy_doc/dsl_spec.rb +143 -0
- data/spec/lib/lazy_doc/memoizer_spec.rb +22 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/support/shared_examples.rb +5 -0
- metadata +116 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
MTYzNGFkNWU2YjdkM2I4OGE4YzAyY2UyNmRmOGNmOTFkMWI4ZDQ3Yg==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
Nzk4YzFiNjhkMzg1YTI2YTI0YWIwY2QzZGIzYjk1OGE0NDVjYTNmMw==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
NmY4NmIwMGRjYmEzYTEwMWI2N2MxY2ZhYTRjYjc2NGNhZjU2NzBjYTllMmM0
|
10
|
+
MzdlOWI1ZmY2ZGMzZmUxM2IwMjE1YWQ5OTE3NTRkZjBjYmIxZWNiNjkyOTk0
|
11
|
+
Y2RjOTYwMzI1M2ViYTAyYjNjZWVlZDBhNzdmMTdlZDY1NzdkZmI=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
N2UzNDQ5OGVkNDlkZGQ3MmU0NzU2YzIyNTJiZDI2NDZmN2ZlNTZiN2ZhMmNi
|
14
|
+
ZjkwNDY4M2EwY2YxYWI4MGQxYmUwOWZlNTRmNGE5YTk2ZmVjYzFkODA3ZGZh
|
15
|
+
NDljN2U5ODg5NGQwYWJlOGJhN2IwN2IzMjVlM2I3ZTMzMTViOTY=
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
lazy_doc
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-1.9.3-p448
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Ryan Oglesby
|
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,84 @@
|
|
1
|
+
# LazyDoc
|
2
|
+
|
3
|
+
NOTE: LazyDoc is currently in alpha and is not quite ready for use.
|
4
|
+
|
5
|
+
[![Build Status](https://travis-ci.org/ryanoglesby08/lazy-doc.png)](https://travis-ci.org/ryanoglesby08/lazy-doc)
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
An implementation of the [Embedded Document](http://martinfowler.com/bliki/EmbeddedDocument.html) pattern for POROs.
|
10
|
+
|
11
|
+
LazyDoc provides a declarative DSL for extracting deeply nested values from a JSON document. The embedded JSON is lazily
|
12
|
+
parsed so that needed attributes from the document are only parsed when accessed. Finally, parsed values are cached
|
13
|
+
so that subsequent access does not access the JSON again.
|
14
|
+
|
15
|
+
*Currently, LazyDoc only supports JSON. XML support will be added later.*
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
Add this line to your application's Gemfile:
|
20
|
+
|
21
|
+
gem 'lazy_doc'
|
22
|
+
|
23
|
+
## DSL Options
|
24
|
+
|
25
|
+
1. Basic usage. `access`: `access :name` will look for a property called 'name' at the top level of the embedded document.
|
26
|
+
2. `access :name, :phone, :address` will look for all three properties. *This option does not currently support using any options.*
|
27
|
+
3. `via`: `access :job, via: [:profile, :occupation]` will look for a property called 'job' by parsing through
|
28
|
+
'profile' -> 'occupation'.
|
29
|
+
4. `default`: `access :currency, default: 'USD'` will use the default value of 'USD' if the currency attribute is set to an empty value (`empty?` or `nil?`)
|
30
|
+
5. `finally`: `access :initials, finally: lambda { |initials| initials.upcase }` will call the supplied block, passing in
|
31
|
+
'initials,' and will return the result of that block.
|
32
|
+
6. `as`: `access :profile, as: Profile` will pass the sub-document found at 'profile' into a new 'Profile' object, and will return
|
33
|
+
the newly constructed Profile object. This is great for constructing nested LazyDoc relationships.
|
34
|
+
7. `extract`: `access :customers, extract: :name` will make the assumption that the attribute 'customers' will be an array of objects and will extract the 'name' property from each object and return an array of 'names' (This would be the equivalent of the Enumerable#map method)
|
35
|
+
|
36
|
+
|
37
|
+
|
38
|
+
## Example Usage
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
class User
|
42
|
+
include LazyDoc::DSL
|
43
|
+
|
44
|
+
access :name # Access the attribute "name"
|
45
|
+
access :address, via: :streetAddress # Access the attribute "streetAddress"
|
46
|
+
access :job, via: [:profile, :occupation, :title] # Access the attribute "title" found at "profile" -> "occupation"
|
47
|
+
|
48
|
+
def initialize(json)
|
49
|
+
lazily_embed(json) # Initialize the LazyDoc object
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
json = '{"name": "George Washington", "streetAddress": "The White House", "profile": {"occupation": {"title": "President"}}}'
|
54
|
+
user = User.new(json)
|
55
|
+
puts user.name
|
56
|
+
puts user.address
|
57
|
+
puts user.job
|
58
|
+
```
|
59
|
+
|
60
|
+
## To Do
|
61
|
+
|
62
|
+
1. DONE - Full path parsing more than just top level. ex: `access :name, via: [:profile, :basic_info, :name]`
|
63
|
+
2. DONE - Error throwing for incorrectly specified paths
|
64
|
+
3. DONE - Default value if json is null or empty. ex: `access :currency, default: 'USD'`
|
65
|
+
4. DONE - Transforms. ex: `access :name, finally: lambda { |name| name.gsub('-',' ') }`
|
66
|
+
5. DONE - Objects from sub-trees. ex: `access :profile, as: Profile` (This would construct a LazyDoc Profile object and pass the json found at "profile" to it)
|
67
|
+
6. Collections.
|
68
|
+
- DONE - Map. For example, extract array of customer names from array of customers. ex: `access :customers, extract: :name`
|
69
|
+
- Objects from collection. Instead of extracting just the name, extract whole objects like in #5. ex: `access :customers, as: Customer`
|
70
|
+
- Other Collection manipulations, select, inject, etc
|
71
|
+
7. Joins
|
72
|
+
- Using previously defined attributes. ex: `join :address, from: [:street, :city, :state:, :zip]`
|
73
|
+
- Defining attributes in place.
|
74
|
+
8. DONE - Multiple simple paths in one line (ex: `access :name, :street, :city, :state`)
|
75
|
+
9. Infer camelCase to snake_case and vice versa in JSON ex: `access :customer_name` (Where the json has customerName)
|
76
|
+
10. XML support
|
77
|
+
|
78
|
+
## Contributing
|
79
|
+
|
80
|
+
1. Fork it
|
81
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
82
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
83
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
84
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/lazy_doc.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/lazy_doc/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Ryan Oglesby"]
|
6
|
+
gem.email = ["ryan.oglesby08@gmail.com"]
|
7
|
+
gem.description = %q{LazyDoc provides a declarative DSL for extracting deeply nested values from a JSON document}
|
8
|
+
gem.summary = %q{An implementation of the Embedded Document pattern for POROs}
|
9
|
+
gem.homepage = "https://github.com/ryanoglesby08/lazy-doc"
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "lazy_doc"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = LazyDoc::VERSION
|
17
|
+
|
18
|
+
gem.license = 'MIT'
|
19
|
+
|
20
|
+
gem.add_development_dependency "rspec"
|
21
|
+
gem.add_development_dependency "pry-debugger"
|
22
|
+
end
|
data/lib/lazy_doc.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
module LazyDoc
|
2
|
+
module Commands
|
3
|
+
end
|
4
|
+
end
|
5
|
+
|
6
|
+
require 'lazy_doc/commands/as_class_command'
|
7
|
+
require 'lazy_doc/commands/default_value_command'
|
8
|
+
require 'lazy_doc/commands/extract_command'
|
9
|
+
require 'lazy_doc/commands/finally_command'
|
10
|
+
require 'lazy_doc/commands/via_command'
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module LazyDoc::Commands
|
2
|
+
class DefaultValueCommand
|
3
|
+
attr_reader :default
|
4
|
+
|
5
|
+
def initialize(default)
|
6
|
+
@default = default
|
7
|
+
end
|
8
|
+
|
9
|
+
def execute(value)
|
10
|
+
use_default?(value) ? default : value
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def use_default?(value)
|
16
|
+
!default.nil? && (value.nil? || value.empty?)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module LazyDoc::Commands
|
2
|
+
class ExtractCommand
|
3
|
+
|
4
|
+
attr_reader :attribute_to_extract
|
5
|
+
|
6
|
+
def initialize(attribute_to_extract)
|
7
|
+
@attribute_to_extract = attribute_to_extract
|
8
|
+
end
|
9
|
+
|
10
|
+
def execute(value)
|
11
|
+
attribute_to_extract.nil? ? value : value.map { |element| element[attribute_to_extract.to_s] }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module LazyDoc::Commands
|
2
|
+
class FinallyCommand
|
3
|
+
attr_reader :transformation
|
4
|
+
|
5
|
+
NO_OP_TRANSFORMATION = lambda { |value| value }
|
6
|
+
|
7
|
+
def initialize(transformation)
|
8
|
+
@transformation = transformation || NO_OP_TRANSFORMATION
|
9
|
+
end
|
10
|
+
|
11
|
+
def execute(value)
|
12
|
+
transformation.call(value)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module LazyDoc::Commands
|
2
|
+
class ViaCommand
|
3
|
+
attr_reader :path
|
4
|
+
|
5
|
+
def initialize(path)
|
6
|
+
@path = [path].flatten
|
7
|
+
end
|
8
|
+
|
9
|
+
def execute(document)
|
10
|
+
path.inject(document) do |final_value, attribute|
|
11
|
+
unless final_value.has_key?(attribute.to_s)
|
12
|
+
raise LazyDoc::AttributeNotFoundError.new("Unable to access #{attribute} via #{path.join(', ')}")
|
13
|
+
end
|
14
|
+
|
15
|
+
final_value[attribute.to_s]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
data/lib/lazy_doc/dsl.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module LazyDoc
|
4
|
+
module DSL
|
5
|
+
def self.included(base)
|
6
|
+
base.extend ClassMethods
|
7
|
+
end
|
8
|
+
|
9
|
+
def lazily_embed(json)
|
10
|
+
@_embedded_doc_source = json
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def memoizer
|
16
|
+
@_memoizer ||= Memoizer.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def embedded_doc
|
20
|
+
@_embedded_doc ||= JSON.parse(@_embedded_doc_source)
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
module ClassMethods
|
25
|
+
def access(*arguments)
|
26
|
+
attributes, options = extract_from(arguments)
|
27
|
+
|
28
|
+
if attributes.size == 1
|
29
|
+
define_access_method_for(attributes[0], options)
|
30
|
+
else
|
31
|
+
define_access_methods_for(attributes)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def define_access_method_for(attribute, options)
|
36
|
+
create_method(attribute, Commands::ViaCommand.new(options[:via] || attribute),
|
37
|
+
Commands::DefaultValueCommand.new(options[:default]),
|
38
|
+
Commands::AsClassCommand.new(options[:as]),
|
39
|
+
Commands::ExtractCommand.new(options[:extract]),
|
40
|
+
Commands::FinallyCommand.new(options[:finally]))
|
41
|
+
end
|
42
|
+
|
43
|
+
def define_access_methods_for(attributes)
|
44
|
+
attributes.each do |attribute|
|
45
|
+
create_method(attribute, Commands::ViaCommand.new(attribute))
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
def create_method(attribute, required_command, *optional_commands)
|
51
|
+
define_method attribute do
|
52
|
+
memoizer.memoize attribute do
|
53
|
+
value = required_command.execute(embedded_doc)
|
54
|
+
|
55
|
+
optional_commands.inject(value) do |final_value, command|
|
56
|
+
final_value = command.execute(final_value)
|
57
|
+
final_value
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
def extract_from(arguments)
|
66
|
+
arguments << {} unless arguments.last.is_a? Hash
|
67
|
+
|
68
|
+
options = arguments.pop
|
69
|
+
attributes = [arguments].flatten
|
70
|
+
|
71
|
+
verify_arguments(attributes, options)
|
72
|
+
|
73
|
+
[attributes, options]
|
74
|
+
end
|
75
|
+
|
76
|
+
def verify_arguments(attributes, options)
|
77
|
+
if attributes.size > 1 && !options.empty?
|
78
|
+
raise ArgumentError, 'Options provided for multiple attributes'
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module LazyDoc
|
2
|
+
class Memoizer
|
3
|
+
def memoize(attribute)
|
4
|
+
attribute_variable_name = "@#{attribute}"
|
5
|
+
unless instance_variable_defined?(attribute_variable_name)
|
6
|
+
instance_variable_set(attribute_variable_name, yield)
|
7
|
+
end
|
8
|
+
|
9
|
+
instance_variable_get(attribute_variable_name)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
|
3
|
+
describe 'basic behavior with a simple JSON document' do
|
4
|
+
class Friends
|
5
|
+
include LazyDoc::DSL
|
6
|
+
|
7
|
+
access :best_friend, via: :bestFriend
|
8
|
+
access :lover
|
9
|
+
|
10
|
+
def initialize(json)
|
11
|
+
lazily_embed(json)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class User
|
16
|
+
include LazyDoc::DSL
|
17
|
+
|
18
|
+
access :name
|
19
|
+
access :phone, :zip
|
20
|
+
access :home_town
|
21
|
+
access :address, via: :streetAddress
|
22
|
+
access :job_title, via: [:profile, :occupation, :title]
|
23
|
+
access :born_on, via: [:profile, :bornOn], finally: lambda { |born_on| born_on.to_i }
|
24
|
+
access :friends, as: Friends
|
25
|
+
access :father, default: 'Chuck Palahniuk'
|
26
|
+
access :fight_club_rules, via: [:fightClub, :rules], extract: :title
|
27
|
+
|
28
|
+
def initialize(json)
|
29
|
+
lazily_embed(json)
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
let(:json_file) { File.read(File.join(File.dirname(__FILE__), 'support/user.json')) }
|
35
|
+
|
36
|
+
subject(:user) { User.new(json_file) }
|
37
|
+
|
38
|
+
its(:name) { should == 'Tyler Durden' }
|
39
|
+
its(:phone) { should == '288-555-0153' }
|
40
|
+
its(:zip) { should == '00000' }
|
41
|
+
its(:address) { should == 'Paper Street' }
|
42
|
+
its(:job_title) { should == 'Soap Maker' }
|
43
|
+
its(:born_on) { should == 1999 }
|
44
|
+
specify { expect { user.home_town }.to raise_error(LazyDoc::AttributeNotFoundError) }
|
45
|
+
its(:father) { should == 'Chuck Palahniuk' }
|
46
|
+
|
47
|
+
context 'friends' do
|
48
|
+
let(:friends) { user.friends }
|
49
|
+
|
50
|
+
specify { expect(friends.best_friend).to eq('Brad Pitt') }
|
51
|
+
specify { expect(friends.lover).to eq('Helena Bonham Carter') }
|
52
|
+
end
|
53
|
+
|
54
|
+
its(:fight_club_rules) { should == ['You do not talk about Fight Club',
|
55
|
+
'You DO NOT talk about Fight Club',
|
56
|
+
'If someone says stop or goes limp, taps out the fight is over'] }
|
57
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
{
|
2
|
+
"name": "Tyler Durden",
|
3
|
+
"phone": "288-555-0153",
|
4
|
+
"streetAddress": "Paper Street",
|
5
|
+
"zip": "00000",
|
6
|
+
"profile": {
|
7
|
+
"bornOn": "1999",
|
8
|
+
"occupation": {
|
9
|
+
"title": "Soap Maker",
|
10
|
+
"salary": 0
|
11
|
+
}
|
12
|
+
},
|
13
|
+
"father": null,
|
14
|
+
"friends": {
|
15
|
+
"bestFriend": "Brad Pitt",
|
16
|
+
"lover": "Helena Bonham Carter"
|
17
|
+
},
|
18
|
+
"fightClub": {
|
19
|
+
"rules": [
|
20
|
+
{"number": 1, "title": "You do not talk about Fight Club"},
|
21
|
+
{"number": 2, "title": "You DO NOT talk about Fight Club"},
|
22
|
+
{"number": 3, "title": "If someone says stop or goes limp, taps out the fight is over"}
|
23
|
+
]
|
24
|
+
}
|
25
|
+
}
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require_relative '../../../spec_helper'
|
2
|
+
|
3
|
+
module LazyDoc
|
4
|
+
describe Commands::AsClassCommand do
|
5
|
+
class Foo
|
6
|
+
def initialize(value)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
let(:value) { {foo: 'bar'} }
|
11
|
+
|
12
|
+
context 'acts like a Command' do
|
13
|
+
subject(:command) { Commands::AsClassCommand.new(Foo) }
|
14
|
+
|
15
|
+
it_behaves_like 'the Command interface'
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'constructs an object of the specified class' do
|
19
|
+
as_class_command = Commands::AsClassCommand.new(Foo)
|
20
|
+
|
21
|
+
as_class_value = as_class_command.execute(value)
|
22
|
+
|
23
|
+
expect(as_class_value).to be_a Foo
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'returns the original value when the specified class is blank' do
|
27
|
+
as_class_command = Commands::AsClassCommand.new(nil)
|
28
|
+
|
29
|
+
as_class_value = as_class_command.execute(value)
|
30
|
+
|
31
|
+
expect(as_class_value).to eq(value)
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'passes the value as json to the specified class' do
|
35
|
+
as_class_command = Commands::AsClassCommand.new(Foo)
|
36
|
+
|
37
|
+
Foo.should_receive(:new).with("{\"foo\":\"bar\"}")
|
38
|
+
|
39
|
+
as_class_command.execute(value)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require_relative '../../../spec_helper'
|
2
|
+
|
3
|
+
module LazyDoc
|
4
|
+
describe Commands::DefaultValueCommand do
|
5
|
+
context 'acts like a Command' do
|
6
|
+
subject(:command) { Commands::DefaultValueCommand.new('default value') }
|
7
|
+
|
8
|
+
it_behaves_like 'the Command interface'
|
9
|
+
end
|
10
|
+
|
11
|
+
let(:default) { Commands::DefaultValueCommand.new(default_value) }
|
12
|
+
|
13
|
+
context 'when there is a default value' do
|
14
|
+
let(:default_value) { 'default value' }
|
15
|
+
|
16
|
+
it 'returns the default value when the supplied value is nil' do
|
17
|
+
final_value = default.execute(nil)
|
18
|
+
|
19
|
+
expect(final_value).to eq(default_value)
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'returns the default value when the supplied value is empty' do
|
23
|
+
final_value = default.execute('')
|
24
|
+
|
25
|
+
expect(final_value).to eq(default_value)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'returns the supplied value when it is not nil' do
|
29
|
+
final_value = default.execute('supplied value')
|
30
|
+
|
31
|
+
expect(final_value).to eq('supplied value')
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'when there is not a default value' do
|
37
|
+
let(:default_value) { nil }
|
38
|
+
|
39
|
+
it 'returns the supplied value even when the supplied value is nil' do
|
40
|
+
final_value = default.execute(nil)
|
41
|
+
|
42
|
+
expect(final_value).to eq(nil)
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'returns the supplied value even when the supplied value is empty' do
|
46
|
+
final_value = default.execute('')
|
47
|
+
|
48
|
+
expect(final_value).to eq('')
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'returns the supplied value when the supplied value is not nil or empty' do
|
52
|
+
final_value = default.execute('supplied value')
|
53
|
+
|
54
|
+
expect(final_value).to eq('supplied value')
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require_relative '../../../spec_helper'
|
2
|
+
|
3
|
+
module LazyDoc
|
4
|
+
describe Commands::ExtractCommand do
|
5
|
+
context 'acts like a Command' do
|
6
|
+
subject(:command) { Commands::ExtractCommand.new('attribute to extract') }
|
7
|
+
|
8
|
+
it_behaves_like 'the Command interface'
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'returns an array of the extracted attribute' do
|
12
|
+
extract = Commands::ExtractCommand.new(:name)
|
13
|
+
|
14
|
+
names = [{'name' => 'Brian'}, {'name' => 'Chris'}, {'name' => 'Mary'}]
|
15
|
+
extracted_names = extract.execute(names)
|
16
|
+
|
17
|
+
expect(extracted_names).to eq(['Brian', 'Chris', 'Mary'])
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'returns the original value when there is no specified attribute to extract' do
|
21
|
+
extract = Commands::ExtractCommand.new(nil)
|
22
|
+
|
23
|
+
extracted_value = extract.execute('foo')
|
24
|
+
|
25
|
+
expect(extracted_value).to eq('foo')
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require_relative '../../../spec_helper'
|
2
|
+
|
3
|
+
module LazyDoc
|
4
|
+
describe Commands::FinallyCommand do
|
5
|
+
let(:value) { 'hello world' }
|
6
|
+
|
7
|
+
context 'acts like a Command' do
|
8
|
+
subject(:command) { Commands::FinallyCommand.new(nil) }
|
9
|
+
|
10
|
+
it_behaves_like 'the Command interface'
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'transforms the supplied value with the supplied transformation' do
|
14
|
+
transformation = lambda { |value| value.upcase }
|
15
|
+
finally = Commands::FinallyCommand.new(transformation)
|
16
|
+
|
17
|
+
final_value = finally.execute(value)
|
18
|
+
|
19
|
+
expect(final_value).to eq('HELLO WORLD')
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'uses a no op transformation when the supplied transformation is blank' do
|
23
|
+
finally = Commands::FinallyCommand.new(nil)
|
24
|
+
|
25
|
+
final_value = finally.execute(value)
|
26
|
+
|
27
|
+
expect(final_value).to eq('hello world')
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require_relative '../../../spec_helper'
|
2
|
+
|
3
|
+
module LazyDoc
|
4
|
+
describe Commands::ViaCommand do
|
5
|
+
context 'acts like a Command' do
|
6
|
+
subject(:command) { Commands::ViaCommand.new(:foo) }
|
7
|
+
|
8
|
+
it_behaves_like 'the Command interface'
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'extracts an attribute from a document' do
|
12
|
+
document = {'foo' => 'bar'}
|
13
|
+
via = Commands::ViaCommand.new(:foo)
|
14
|
+
|
15
|
+
final_value = via.execute(document)
|
16
|
+
|
17
|
+
expect(final_value).to eq('bar')
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'extracts a nested attribute from a document' do
|
21
|
+
document = {'foo' => {'bar' => {'blarg' => 'baz'}}}
|
22
|
+
via = Commands::ViaCommand.new([:foo, :bar, :blarg])
|
23
|
+
|
24
|
+
final_value = via.execute(document)
|
25
|
+
|
26
|
+
expect(final_value).to eq('baz')
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'raises AttributeNotFoundError when the document does not contain the path' do
|
30
|
+
document = {'foo' => {'bar' => {'blarg' => 'baz'}}}
|
31
|
+
via = Commands::ViaCommand.new([:foo, :wizz])
|
32
|
+
|
33
|
+
expect { via.execute(document) }.to raise_error(AttributeNotFoundError)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
require_relative '../../spec_helper'
|
2
|
+
|
3
|
+
module LazyDoc
|
4
|
+
describe DSL do
|
5
|
+
describe '.access' do
|
6
|
+
let(:json) { '{"foo":"bar", "blarg":"wibble"}' }
|
7
|
+
|
8
|
+
subject(:test_find) { Object.new }
|
9
|
+
|
10
|
+
before do
|
11
|
+
class << test_find
|
12
|
+
include LazyDoc::DSL
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'defines a method for the name of the attribute' do
|
17
|
+
test_find.singleton_class.access :foo
|
18
|
+
|
19
|
+
expect(test_find).to respond_to :foo
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'assumes the attribute name is sufficient to find the attribute' do
|
23
|
+
test_find.singleton_class.access :foo
|
24
|
+
test_find.lazily_embed(json)
|
25
|
+
|
26
|
+
expect(test_find.foo).to eq("bar")
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'caches the json attribute for subsequent access' do
|
30
|
+
test_find.singleton_class.access :foo
|
31
|
+
test_find.lazily_embed(json)
|
32
|
+
|
33
|
+
expect(test_find.foo).to eq("bar")
|
34
|
+
|
35
|
+
test_find.stub(:embedded_doc) { nil }
|
36
|
+
|
37
|
+
expect(test_find.foo).to eq("bar")
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'provides simple access to more than one attribute' do
|
41
|
+
test_find.singleton_class.access :foo, :blarg
|
42
|
+
test_find.lazily_embed(json)
|
43
|
+
|
44
|
+
expect(test_find.foo).to eq("bar")
|
45
|
+
expect(test_find.blarg).to eq("wibble")
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'raises ArgumentError when more than one attribute is accessed with options' do
|
49
|
+
expect { test_find.singleton_class.access :foo, :blarg, as: Foo}.to raise_error(ArgumentError, 'Options provided for multiple attributes')
|
50
|
+
end
|
51
|
+
|
52
|
+
context 'via' do
|
53
|
+
it 'defines a method that accesses a named json attribute' do
|
54
|
+
test_find.singleton_class.access :my_foo, via: :foo
|
55
|
+
test_find.lazily_embed(json)
|
56
|
+
|
57
|
+
expect(test_find.my_foo).to eq("bar")
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'defines a method that accesses a named json attribute through a json path' do
|
61
|
+
json = '{"bar": {"foo":"Hello World"}}'
|
62
|
+
test_find.singleton_class.access :foo, via: [:bar, :foo]
|
63
|
+
test_find.lazily_embed(json)
|
64
|
+
|
65
|
+
expect(test_find.foo).to eq('Hello World')
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
context 'finally' do
|
70
|
+
it 'executes a block on the the attribute at the json path' do
|
71
|
+
test_find.singleton_class.access :foo, finally: lambda { |foo| foo.upcase }
|
72
|
+
test_find.lazily_embed(json)
|
73
|
+
|
74
|
+
expect(test_find.foo).to eq('BAR')
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
context 'as' do
|
79
|
+
let(:json) { '{"foo": {"bar": "Hello"}}'}
|
80
|
+
|
81
|
+
class Foo
|
82
|
+
include LazyDoc::DSL
|
83
|
+
|
84
|
+
access :bar
|
85
|
+
|
86
|
+
def initialize(json)
|
87
|
+
lazily_embed(json)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'embeds a sub-object into another user defined object' do
|
92
|
+
test_find.singleton_class.access :foo, as: Foo
|
93
|
+
test_find.lazily_embed(json)
|
94
|
+
|
95
|
+
foo = test_find.foo
|
96
|
+
|
97
|
+
expect(foo).to be_a(Foo)
|
98
|
+
expect(foo.bar).to eq('Hello')
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'calls the finally method on the sub-object defined by "as"' do
|
102
|
+
class Foo
|
103
|
+
def bar_baz; bar + ' World' end
|
104
|
+
end
|
105
|
+
test_find.singleton_class.access :foo, as: Foo, finally: lambda { |foo| foo.bar_baz }
|
106
|
+
test_find.lazily_embed(json)
|
107
|
+
|
108
|
+
expect(test_find.foo).to eq('Hello World')
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
context 'default' do
|
113
|
+
it 'returns the default value when the json value is null' do
|
114
|
+
json = '{"foo": null}'
|
115
|
+
test_find.singleton_class.access :foo, default: 'hello world'
|
116
|
+
test_find.lazily_embed(json)
|
117
|
+
|
118
|
+
expect(test_find.foo).to eq('hello world')
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'returns the json value when the json value is not null' do
|
122
|
+
test_find.singleton_class.access :foo, default: 'hello world'
|
123
|
+
test_find.lazily_embed(json)
|
124
|
+
|
125
|
+
expect(test_find.foo).to eq('bar')
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
context 'extract' do
|
130
|
+
it 'extracts the specified property name from each element in the array' do
|
131
|
+
json = '{"foo":[{"bar": "1"}, {"bar": "2"}, {"bar": "3"}]}'
|
132
|
+
test_find.singleton_class.access :foo, extract: :bar
|
133
|
+
test_find.lazily_embed(json)
|
134
|
+
|
135
|
+
expect(test_find.foo).to eq(['1','2','3'])
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require_relative '../../spec_helper'
|
2
|
+
|
3
|
+
module LazyDoc
|
4
|
+
describe Memoizer do
|
5
|
+
let(:memoizer) { Memoizer.new }
|
6
|
+
|
7
|
+
it 'returns the value of the block' do
|
8
|
+
memoized_value = memoizer.memoize(:foo) { 'hello world' }
|
9
|
+
|
10
|
+
expect(memoized_value).to eq('hello world')
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'returns the original block value given for subsequent access to the attribute' do
|
14
|
+
memoizer.memoize(:foo) { 'hello world' }
|
15
|
+
|
16
|
+
memoized_value = memoizer.memoize(:foo) { :doesnt_matter }
|
17
|
+
|
18
|
+
expect(memoized_value).to eq('hello world')
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: lazy_doc
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ryan Oglesby
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-09-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ! '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ! '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: pry-debugger
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description: LazyDoc provides a declarative DSL for extracting deeply nested values
|
42
|
+
from a JSON document
|
43
|
+
email:
|
44
|
+
- ryan.oglesby08@gmail.com
|
45
|
+
executables: []
|
46
|
+
extensions: []
|
47
|
+
extra_rdoc_files: []
|
48
|
+
files:
|
49
|
+
- .gitignore
|
50
|
+
- .rspec
|
51
|
+
- .ruby-gemset
|
52
|
+
- .ruby-version
|
53
|
+
- .travis.yml
|
54
|
+
- Gemfile
|
55
|
+
- LICENSE
|
56
|
+
- README.md
|
57
|
+
- Rakefile
|
58
|
+
- lazy_doc.gemspec
|
59
|
+
- lib/lazy_doc.rb
|
60
|
+
- lib/lazy_doc/attribute_not_found_error.rb
|
61
|
+
- lib/lazy_doc/commands.rb
|
62
|
+
- lib/lazy_doc/commands/as_class_command.rb
|
63
|
+
- lib/lazy_doc/commands/default_value_command.rb
|
64
|
+
- lib/lazy_doc/commands/extract_command.rb
|
65
|
+
- lib/lazy_doc/commands/finally_command.rb
|
66
|
+
- lib/lazy_doc/commands/via_command.rb
|
67
|
+
- lib/lazy_doc/dsl.rb
|
68
|
+
- lib/lazy_doc/memoizer.rb
|
69
|
+
- lib/lazy_doc/version.rb
|
70
|
+
- spec/acceptance/basic_behavior_spec.rb
|
71
|
+
- spec/acceptance/support/user.json
|
72
|
+
- spec/lib/lazy_doc/commands/as_class_command_spec.rb
|
73
|
+
- spec/lib/lazy_doc/commands/default_value_command_spec.rb
|
74
|
+
- spec/lib/lazy_doc/commands/extract_command_spec.rb
|
75
|
+
- spec/lib/lazy_doc/commands/finally_command_spec.rb
|
76
|
+
- spec/lib/lazy_doc/commands/via_command_spec.rb
|
77
|
+
- spec/lib/lazy_doc/dsl_spec.rb
|
78
|
+
- spec/lib/lazy_doc/memoizer_spec.rb
|
79
|
+
- spec/spec_helper.rb
|
80
|
+
- spec/support/shared_examples.rb
|
81
|
+
homepage: https://github.com/ryanoglesby08/lazy-doc
|
82
|
+
licenses:
|
83
|
+
- MIT
|
84
|
+
metadata: {}
|
85
|
+
post_install_message:
|
86
|
+
rdoc_options: []
|
87
|
+
require_paths:
|
88
|
+
- lib
|
89
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ! '>='
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
requirements: []
|
100
|
+
rubyforge_project:
|
101
|
+
rubygems_version: 2.0.6
|
102
|
+
signing_key:
|
103
|
+
specification_version: 4
|
104
|
+
summary: An implementation of the Embedded Document pattern for POROs
|
105
|
+
test_files:
|
106
|
+
- spec/acceptance/basic_behavior_spec.rb
|
107
|
+
- spec/acceptance/support/user.json
|
108
|
+
- spec/lib/lazy_doc/commands/as_class_command_spec.rb
|
109
|
+
- spec/lib/lazy_doc/commands/default_value_command_spec.rb
|
110
|
+
- spec/lib/lazy_doc/commands/extract_command_spec.rb
|
111
|
+
- spec/lib/lazy_doc/commands/finally_command_spec.rb
|
112
|
+
- spec/lib/lazy_doc/commands/via_command_spec.rb
|
113
|
+
- spec/lib/lazy_doc/dsl_spec.rb
|
114
|
+
- spec/lib/lazy_doc/memoizer_spec.rb
|
115
|
+
- spec/spec_helper.rb
|
116
|
+
- spec/support/shared_examples.rb
|