rep 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
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
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in representative.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 myobie
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,168 @@
1
+ # Rep
2
+
3
+ ```
4
+ /T /I
5
+ / |/ | .-~/
6
+ T\ Y I |/ / _
7
+ /T | \I | I Y.-~/
8
+ I l /I T\ | | l | T /
9
+ __ | \l \l \I l __l l \ ` _. |
10
+ \ ~-l `\ `\ \ \\ ~\ \ `. .-~ |
11
+ \ ~-. "-. ` \ ^._ ^. "-. / \ |
12
+ .--~-._ ~- ` _ ~-_.-"-." ._ /._ ." ./
13
+ >--. ~-. ._ ~>-" "\\ 7 7 ]
14
+ ^.___~"--._ ~-{ .-~ . `\ Y . / |
15
+ <__ ~"-. ~ /_/ \ \I Y : |
16
+ ^-.__ ~(_/ \ >._: | l______
17
+ ^--.,___.-~" /_/ ! `-.~"--l_ / ~"-.
18
+ (_/ . ~( /' "~"--,Y -=b-. _)
19
+ (_/ . \ : / l c"~o \
20
+ \ / `. . .^ \_.-~"~--. )
21
+ (_/ . ` / / ! )/
22
+ / / _. '. .': / '
23
+ ~(_/ . / _ ` .-<_
24
+ /_/ . ' .-~" `. / \ \ ,z=.
25
+ ~( / ' : | K "-.~-.______//
26
+ "-,. l I/ \_ __{--->._(==.
27
+ //( \ < ~"~" //
28
+ /' /\ \ \ ,v=. ((
29
+ .^. / /\ " }__ //===- `
30
+ / / ' ' "-.,__ {---(==-
31
+ .^ ' : T ~" ll
32
+ / . . . : | :! \\
33
+ (_/ / | | j-" ~^
34
+ ~-<_(_.^-~"
35
+ ```
36
+
37
+ A library for writing authoritative representations of objects for pages and apis.
38
+
39
+ ## Installation
40
+
41
+ Add this line to your application's Gemfile:
42
+
43
+ gem 'rep'
44
+
45
+ And then execute:
46
+
47
+ $ bundle
48
+
49
+ Or install it yourself as:
50
+
51
+ $ gem install rep
52
+
53
+ ## Usage
54
+
55
+ `include Rep` into any class and it is endowed with a `#to_json` method,
56
+ among other things. You describe the top level keys you want for your
57
+ json with the `::fields` method. The values for the fields are expected
58
+ to be returned from methods on the object of the same name.
59
+
60
+ If a class has `fields :one => :default`, then `def one; 1; end` is
61
+ expected.
62
+
63
+ ## Examples
64
+
65
+ ```ruby
66
+ # imagine Photo is an ActiveRecord model with fields for title, exif, location, and user_id
67
+
68
+ class PhotoRep
69
+ include Rep
70
+
71
+ initialize_with :photo
72
+
73
+ json_fields [:url, :title, :exif, :location, :user] => :default
74
+
75
+ forward [:title, :exif, :location] => :photo
76
+ forward :user => :user_rep
77
+
78
+ def url
79
+ full_photo_url(photo.name)
80
+ end
81
+
82
+ def user
83
+ UserRep.shared(user: photo.user)
84
+ end
85
+ end
86
+
87
+ # imagine User is an ActiveRecord model with fields for name, email, and location
88
+
89
+ class UserRep
90
+ include Rep
91
+
92
+ initialize_with :user
93
+
94
+ json_fields [:name, :email, :location] => :default
95
+ json_fields [:id, :admin].concat(json_fields(:default)) => :admin
96
+
97
+ forward json_fields(:admin) => :user
98
+ end
99
+
100
+ # You can now do crazy stuff like
101
+
102
+ UserRep.new(user: User.first).to_hash.keys # => [:name, :email, :location]
103
+
104
+ # To save from creating lots of objects, you can use a shared class that is reset fresh
105
+ UserRep.shared(user: User.first).to_hash # => { name: "Nathan Herald:, ...
106
+
107
+ # You can use class to proc (that makes a hash using the shared class)
108
+ User.all.map(&UserRep) # => [{ name: "Nathan Herald" ...
109
+
110
+ # or maybe find all photos which will embed all users (and only ever make one instance each of PhotoRep and UserRep)
111
+ Photo.all.map(&PhotoRep).to_json
112
+ ```
113
+
114
+ ### Any class
115
+
116
+ You don't have to have a Rep per model and Rep's can represent multiple objects at once. It's POROs.
117
+
118
+ ```ruby
119
+ class ProjectReport
120
+ initialize_with :project, :active_users, :orders
121
+ fields [:name, :date, :count, :total_gross_cost, :cost_per_active_user]
122
+ forward :date => :project
123
+ forward :count => :orders
124
+
125
+ def name
126
+ "#{project.name} Report"
127
+ end
128
+
129
+ def total_gross_cost
130
+ orders.reduce(0.0) { |memo, order| memo += order.total_gross_cost.to_f }
131
+ end
132
+
133
+ def cost_per_active_user
134
+ active_users.count.to_f / total_gross_cost.to_f
135
+ end
136
+ end
137
+ ```
138
+
139
+ ### Hashie
140
+
141
+ If you have the hashie gem in your project, `#to_hash` returns a `Hashie::Mash` instance.
142
+
143
+ ```ruby
144
+ UserRep.new(user: User.first).to_hash.class # => Hashie::Mash
145
+
146
+ # which lets you do the dot accessors for things
147
+ @user = UserRep.new(user: User.first).to_hash
148
+ @user.name # => "Nathan Herald"
149
+ ```
150
+
151
+ ### Rails
152
+
153
+ A possible controller
154
+ ```ruby
155
+ class PhotosController < ApplicationController
156
+ respond_to :json, :html
157
+
158
+ def index
159
+ @photos = Photo.paginate(page: params[:page], per_page: 20)
160
+ respond_with @photos.map(&PhotoRep)
161
+ end
162
+
163
+ def show
164
+ @photo = Photo.find(params[:id])
165
+ respond_with PhotoRep.new(photo: @photo)
166
+ end
167
+ end
168
+ ```
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+ Rake::TestTask.new do |t|
4
+ t.libs << 'test'
5
+ t.test_files = FileList['test/**/*_test.rb']
6
+ t.verbose = true
7
+ end
8
+ task default: 'test'
@@ -0,0 +1,3 @@
1
+ module Rep
2
+ VERSION = "0.0.1"
3
+ end
data/lib/rep.rb ADDED
@@ -0,0 +1,150 @@
1
+ require "rep/version"
2
+ require 'forwardable'
3
+ require 'json'
4
+
5
+ module Rep
6
+ def self.included(klass)
7
+ klass.extend Forwardable
8
+ klass.extend ClassMethods
9
+ klass.instance_eval {
10
+ class << self
11
+ alias forward delegate
12
+
13
+ unless defined?(fields)
14
+ alias fields json_fields
15
+ end
16
+
17
+ if defined?(Hashie)
18
+ include HashieSupport
19
+ end
20
+ end
21
+
22
+ unless defined?(parse_opts)
23
+ def parse_opts(opts = {})
24
+ # NOOP
25
+ end
26
+ end
27
+ }
28
+ end
29
+
30
+ def reset_for_json!
31
+ self.class.all_json_methods.each do |method_name|
32
+ instance_variable_set(:"@#{method_name}", nil)
33
+ end
34
+ end
35
+
36
+ def to_hash(name = :default)
37
+ if fields = self.class.json_fields(name)
38
+ fields.reduce({}) do |memo, field|
39
+ field_name, method_name = field.is_a?(Hash) ? field.to_a.first : [field, field]
40
+ begin
41
+ memo[field_name] = send(method_name)
42
+ rescue NoMethodError => e
43
+ message = "There is no method named '#{method_name}' for the class '#{self.class}' for the '#{name}' list of fields : #{e.message}"
44
+ raise NoMethodError.new(message, method_name, e.args)
45
+ end
46
+ memo
47
+ end
48
+ else
49
+ raise "There are no json fields under the name: #{name}"
50
+ end
51
+ end
52
+
53
+ def to_json
54
+ JSON.generate(to_hash)
55
+ end
56
+
57
+ module ClassMethods
58
+ def register_accessor(acc)
59
+ name, default = acc.is_a?(Hash) ? acc.to_a.first : [acc, nil]
60
+ attr_accessor name
61
+ if default
62
+ define_method name do
63
+ var_name = :"@#{name}"
64
+ instance_variable_get(var_name) || instance_variable_set(var_name, default)
65
+ end
66
+ end
67
+ end
68
+
69
+ def initialize_with(*args)
70
+ @initializiation_args = args
71
+
72
+ define_singleton_method :initializiation_args do
73
+ @initializiation_args
74
+ end
75
+
76
+ args.each { |a| register_accessor(a) }
77
+
78
+ define_method(:initialize) { |opts = {}| parse_opts(opts) }
79
+
80
+ define_method :parse_opts do |opts|
81
+ @presidential_options = opts
82
+ self.class.initializiation_args.each do |field|
83
+ name = field.is_a?(Hash) ? field.to_a.first.first : field
84
+ instance_variable_set(:"@#{name}", opts[name])
85
+ end
86
+ end
87
+ end
88
+
89
+ def json_fields(arg = nil)
90
+ if arg.is_a?(Hash)
91
+ fields, name = arg.to_a.first
92
+ @json_fields ||= {}
93
+ @json_fields[name] = [fields].flatten
94
+ elsif arg.is_a?(Symbol)
95
+ @json_fields ||= {}
96
+ @json_fields[arg]
97
+ elsif arg === nil
98
+ @json_fields || {}
99
+ else
100
+ # TODO: make an exception class
101
+ raise "You can only use a Hash to set fields, a Symbol to retrieve them, or no argument to retrieve all fields for all names"
102
+ end
103
+ end
104
+
105
+ def flat_json_fields(side = :right)
106
+ side_number = side == :right ? 1 : 0
107
+
108
+ json_fields.reduce([]) do |memo, (name, fields)|
109
+ memo + fields.map do |field|
110
+ if field.is_a?(Hash)
111
+ field.to_a.first[side_number] # [name, method_name]
112
+ else
113
+ field
114
+ end
115
+ end
116
+ end.uniq
117
+ end
118
+
119
+ def all_json_fields
120
+ flat_json_fields(:left)
121
+ end
122
+
123
+ def all_json_methods
124
+ flat_json_fields(:right)
125
+ end
126
+
127
+ # TODO: thread safety
128
+ def shared(opts = {})
129
+ @instance ||= new
130
+ @instance.reset_for_json!
131
+ @instance.parse_opts(opts)
132
+ @instance
133
+ end
134
+
135
+ def to_proc
136
+ proc { |obj|
137
+ arr = [obj].flatten
138
+ init_args = @initializiation_args[0..(arr.length-1)]
139
+ opts = Hash[init_args.zip(arr)]
140
+ shared(opts).to_hash
141
+ }
142
+ end
143
+ end
144
+
145
+ module HashieSupport
146
+ def to_hash(name = :default)
147
+ Hashie::Mash.new(super)
148
+ end
149
+ end
150
+ end
data/rep.gemspec ADDED
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'rep/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "rep"
8
+ gem.version = Rep::VERSION
9
+ gem.authors = ["myobie"]
10
+ gem.email = ["nathan@myobie.com"]
11
+ gem.description = %q{A library for writing authoritative representations of objects for pages and apis.}
12
+ gem.summary = %q{Include Rep into any object to endow it to create json (or hashes) easily.}
13
+ gem.homepage = ""
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+ gem.add_dependency 'json'
20
+ end
@@ -0,0 +1,142 @@
1
+ require 'test_helper'
2
+
3
+ describe Rep do
4
+
5
+ def new_rep_class(&blk)
6
+ klass = Class.new
7
+ klass.send :include, Rep
8
+ if block_given?
9
+ klass.class_eval &blk
10
+ end
11
+ klass
12
+ end
13
+
14
+ it "includes forwardable" do
15
+ new_rep_class.must_respond_to :delegate
16
+ end
17
+
18
+ it "aliases delegate to foward" do
19
+ new_rep_class.must_respond_to :forward
20
+ end
21
+
22
+ it "aliases fields to json_fields" do
23
+ new_rep_class.must_respond_to :fields
24
+ end
25
+
26
+ it "has a parse_opts method" do
27
+ new_rep_class.must_respond_to :parse_opts
28
+ end
29
+
30
+ it "can have fields" do
31
+ klass = new_rep_class do
32
+ fields [:foo, :bar] => :default
33
+ end
34
+ klass.fields(:default).must_equal [:foo, :bar]
35
+ end
36
+
37
+ it "can create initialize method" do
38
+ klass = new_rep_class do
39
+ initialize_with :foo, :bar
40
+ end
41
+ inst = klass.new(foo: 'foo123')
42
+ inst.foo.must_equal 'foo123'
43
+ inst.bar.must_be_nil
44
+ end
45
+
46
+ it "can have default initialization options" do
47
+ klass = new_rep_class do
48
+ initialize_with :foo, { bar: "barbar" }
49
+ end
50
+ inst = klass.new(foo: 'foofoo')
51
+ inst.bar.must_equal 'barbar'
52
+ end
53
+
54
+ it "can overried default initialization options" do
55
+ klass = new_rep_class do
56
+ initialize_with :foo, { bar: "barbar" }
57
+ end
58
+ inst = klass.new(bar: 'notbar')
59
+ inst.bar.must_equal 'notbar'
60
+ inst.foo.must_be_nil
61
+ end
62
+
63
+ it "should accept multiple sets of fields" do
64
+ klass = new_rep_class do
65
+ fields [:one, :four] => :default
66
+ fields [:one, :two, :three] => :superset
67
+ end
68
+ klass.fields(:default).length.must_equal 2
69
+ klass.fields(:superset).length.must_equal 3
70
+ end
71
+
72
+ it "should know all uniq fields" do
73
+ klass = new_rep_class do
74
+ fields [:one, :four] => :default
75
+ fields [:one, :two, :three] => :superset
76
+ end
77
+ klass.all_json_fields.must_equal [:one, :four, :two, :three]
78
+ end
79
+
80
+ it "should send fields to instance to make hash" do
81
+ klass = new_rep_class do
82
+ fields [:one, :two, :three] => :default
83
+ def one; 1; end
84
+ def two; 2; end
85
+ def three; 3; end
86
+ end
87
+ klass.new.to_hash.must_equal one: 1, two: 2, three: 3
88
+ end
89
+
90
+ it "should send fields to instance to make json" do
91
+ klass = new_rep_class do
92
+ fields [:one, :two, :three] => :default
93
+ def one; 1; end
94
+ def two; 2; end
95
+ def three; 3; end
96
+ end
97
+ klass.new.to_json.must_equal '{"one":1,"two":2,"three":3}'
98
+ end
99
+
100
+ it "should let fields alias to method names" do
101
+ klass = new_rep_class do
102
+ fields [{ :one => :real_one }, :two, :three] => :default
103
+ def real_one; 1; end
104
+ def two; 2; end
105
+ def three; 3; end
106
+ end
107
+ klass.new.to_json.must_equal '{"one":1,"two":2,"three":3}'
108
+ end
109
+
110
+ it "should know all the methods for the fields" do
111
+ klass = new_rep_class do
112
+ fields [{ :one => :real_one }, :two, :three] => :default
113
+ def real_one; 1; end
114
+ def two; 2; end
115
+ def three; 3; end
116
+ end
117
+ klass.all_json_methods.must_equal [:real_one, :two, :three]
118
+ end
119
+
120
+ it "should know all the field names for the fields" do
121
+ klass = new_rep_class do
122
+ fields [{ :one => :real_one }, :two, :three] => :default
123
+ def real_one; 1; end
124
+ def two; 2; end
125
+ def three; 3; end
126
+ end
127
+ klass.all_json_fields.must_equal [:one, :two, :three]
128
+ end
129
+
130
+ it "should do symbol to instance craziness" do
131
+ klass = new_rep_class do
132
+ initialize_with :hash
133
+ fields :keys => :default
134
+ forward :keys => :hash
135
+ end
136
+ hashes = [{ one: 1, two: 2 },
137
+ { three: 3, four: 4 },
138
+ { one: 1, five: 5 }]
139
+ hashes.map(&klass).to_json.must_equal '[{"keys":["one","two"]},{"keys":["three","four"]},{"keys":["one","five"]}]'
140
+ end
141
+
142
+ end
@@ -0,0 +1,9 @@
1
+ require 'minitest/autorun'
2
+ require 'minitest/pride'
3
+ require 'rep'
4
+
5
+ class User < Struct.new(:name, :email, :location)
6
+ end
7
+
8
+ class Photo < Struct.new(:name, :location, :exif)
9
+ end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rep
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - myobie
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-11-10 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: json
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ description: A library for writing authoritative representations of objects for pages
31
+ and apis.
32
+ email:
33
+ - nathan@myobie.com
34
+ executables: []
35
+ extensions: []
36
+ extra_rdoc_files: []
37
+ files:
38
+ - .gitignore
39
+ - Gemfile
40
+ - LICENSE.txt
41
+ - README.md
42
+ - Rakefile
43
+ - lib/rep.rb
44
+ - lib/rep/version.rb
45
+ - rep.gemspec
46
+ - test/lib/rep_test.rb
47
+ - test/test_helper.rb
48
+ homepage: ''
49
+ licenses: []
50
+ post_install_message:
51
+ rdoc_options: []
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ! '>='
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ requirements: []
67
+ rubyforge_project:
68
+ rubygems_version: 1.8.23
69
+ signing_key:
70
+ specification_version: 3
71
+ summary: Include Rep into any object to endow it to create json (or hashes) easily.
72
+ test_files:
73
+ - test/lib/rep_test.rb
74
+ - test/test_helper.rb