lazy_doc 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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=
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .idea
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1 @@
1
+ lazy_doc
@@ -0,0 +1 @@
1
+ ruby-1.9.3-p448
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - "1.9.3"
4
+
5
+ script: bundle exec rspec
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in lazy_doc.gemspec
4
+ gemspec
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.
@@ -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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -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
@@ -0,0 +1,5 @@
1
+ require 'lazy_doc/attribute_not_found_error'
2
+ require 'lazy_doc/commands'
3
+ require 'lazy_doc/dsl'
4
+ require 'lazy_doc/memoizer'
5
+ require 'lazy_doc/version'
@@ -0,0 +1,3 @@
1
+ module LazyDoc
2
+ class AttributeNotFoundError < StandardError ;end
3
+ end
@@ -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,13 @@
1
+ module LazyDoc::Commands
2
+ class AsClassCommand
3
+ attr_reader :klass
4
+
5
+ def initialize(klass)
6
+ @klass = klass
7
+ end
8
+
9
+ def execute(value)
10
+ klass.nil? ? value : klass.new(value.to_json)
11
+ end
12
+ end
13
+ end
@@ -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
@@ -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,3 @@
1
+ module LazyDoc
2
+ VERSION = '0.1.0'
3
+ 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
@@ -0,0 +1,4 @@
1
+ require 'pry'
2
+
3
+ require_relative '../lib/lazy_doc'
4
+ require_relative 'support/shared_examples'
@@ -0,0 +1,5 @@
1
+ shared_examples 'the Command interface' do
2
+ it 'responds to execute' do
3
+ expect(command).to respond_to :execute
4
+ end
5
+ end
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