halogen 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +244 -0
- data/Rakefile +6 -0
- data/examples/extensions.md +25 -0
- data/examples/simple.rb +142 -0
- data/halogen.gemspec +32 -0
- data/lib/halogen.rb +157 -0
- data/lib/halogen/collection.rb +44 -0
- data/lib/halogen/configuration.rb +15 -0
- data/lib/halogen/definition.rb +73 -0
- data/lib/halogen/definitions.rb +23 -0
- data/lib/halogen/embeds.rb +105 -0
- data/lib/halogen/embeds/definition.rb +19 -0
- data/lib/halogen/errors.rb +7 -0
- data/lib/halogen/hash_util.rb +37 -0
- data/lib/halogen/links.rb +41 -0
- data/lib/halogen/links/definition.rb +59 -0
- data/lib/halogen/properties.rb +40 -0
- data/lib/halogen/properties/definition.rb +8 -0
- data/lib/halogen/railtie.rb +11 -0
- data/lib/halogen/resource.rb +58 -0
- data/lib/halogen/version.rb +5 -0
- data/spec/halogen/collection_spec.rb +78 -0
- data/spec/halogen/configuration_spec.rb +11 -0
- data/spec/halogen/definition_spec.rb +108 -0
- data/spec/halogen/definitions_spec.rb +24 -0
- data/spec/halogen/embeds/definition_spec.rb +23 -0
- data/spec/halogen/embeds_spec.rb +201 -0
- data/spec/halogen/links/definition_spec.rb +68 -0
- data/spec/halogen/links_spec.rb +59 -0
- data/spec/halogen/properties_spec.rb +41 -0
- data/spec/halogen/resource_spec.rb +77 -0
- data/spec/halogen_spec.rb +127 -0
- data/spec/spec_helper.rb +11 -0
- metadata +179 -0
data/halogen.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
lib = File.expand_path('../lib', __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'halogen/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'halogen'
|
9
|
+
spec.version = Halogen::VERSION
|
10
|
+
spec.authors = ['Heather Rivers']
|
11
|
+
spec.email = ['heather@modeanalytics.com']
|
12
|
+
spec.summary = 'HAL+JSON generator'
|
13
|
+
spec.description = 'Provides a framework-agnostic interface for ' \
|
14
|
+
'generating HAL+JSON representations of resources'
|
15
|
+
spec.homepage = 'https://github.com/mode/halogen'
|
16
|
+
spec.license = 'MIT'
|
17
|
+
|
18
|
+
spec.files = `git ls-files -z`.split("\x0")
|
19
|
+
spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) }
|
20
|
+
spec.test_files = spec.files.grep(/^(test|spec|features)\//)
|
21
|
+
spec.require_paths = ['lib']
|
22
|
+
|
23
|
+
spec.required_ruby_version = '~> 2.0'
|
24
|
+
|
25
|
+
spec.add_dependency 'json', '~> 1.8'
|
26
|
+
|
27
|
+
spec.add_development_dependency 'bundler', '~> 1.7'
|
28
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
29
|
+
spec.add_development_dependency 'rspec', '~> 3.2'
|
30
|
+
spec.add_development_dependency 'simplecov', '~> 0.9'
|
31
|
+
spec.add_development_dependency 'yard', '~> 0.8'
|
32
|
+
end
|
data/lib/halogen.rb
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# encoding: utf-8
|
3
|
+
#
|
4
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__)))
|
5
|
+
|
6
|
+
require 'json'
|
7
|
+
|
8
|
+
# HAL+JSON generator
|
9
|
+
#
|
10
|
+
# Provides a framework-agnostic interface for generating HAL+JSON
|
11
|
+
# representations of resources
|
12
|
+
#
|
13
|
+
module Halogen
|
14
|
+
# Provide Halogen methods for the including module
|
15
|
+
#
|
16
|
+
# @return [Module]
|
17
|
+
#
|
18
|
+
def self.included(base)
|
19
|
+
base.extend ClassMethods
|
20
|
+
|
21
|
+
base.send :include, InstanceMethods
|
22
|
+
base.send :include, Properties
|
23
|
+
base.send :include, Links
|
24
|
+
base.send :include, Embeds
|
25
|
+
|
26
|
+
config.extensions.each { |extension| base.send :include, extension }
|
27
|
+
|
28
|
+
base.send :attr_reader, :options
|
29
|
+
end
|
30
|
+
|
31
|
+
module ClassMethods # :nodoc:
|
32
|
+
# @return [Halogen::Definitions] the definitions container instance
|
33
|
+
#
|
34
|
+
def definitions
|
35
|
+
@definitions ||= Definitions.new
|
36
|
+
end
|
37
|
+
|
38
|
+
# @param [Symbol, String] name of the resource
|
39
|
+
#
|
40
|
+
# @return [Module] self
|
41
|
+
#
|
42
|
+
def resource(name)
|
43
|
+
include Resource
|
44
|
+
|
45
|
+
define_resource(name)
|
46
|
+
end
|
47
|
+
|
48
|
+
# @param [Symbol, String] name of the collection
|
49
|
+
#
|
50
|
+
# @return [Module] self
|
51
|
+
#
|
52
|
+
def collection(name)
|
53
|
+
include Collection
|
54
|
+
|
55
|
+
define_collection(name)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
module InstanceMethods # :nodoc:
|
60
|
+
# @param options [nil, Hash] hash of options
|
61
|
+
#
|
62
|
+
# @return [Object] the representer instance
|
63
|
+
#
|
64
|
+
def initialize(options = {})
|
65
|
+
@options = Halogen::HashUtil.symbolize_keys!(options)
|
66
|
+
end
|
67
|
+
|
68
|
+
# @return [String] rendered JSON
|
69
|
+
#
|
70
|
+
def to_json
|
71
|
+
render.to_json
|
72
|
+
end
|
73
|
+
|
74
|
+
# @return [Hash] rendered representation
|
75
|
+
#
|
76
|
+
def render
|
77
|
+
{}
|
78
|
+
end
|
79
|
+
|
80
|
+
# @return [nil, Object] the parent representer, if this instance is an
|
81
|
+
# embedded child
|
82
|
+
#
|
83
|
+
def parent
|
84
|
+
@parent ||= options.fetch(:parent, nil)
|
85
|
+
end
|
86
|
+
|
87
|
+
# @return [Integer] the depth at which this representer is embedded
|
88
|
+
#
|
89
|
+
def depth
|
90
|
+
@depth ||= parent ? parent.depth + 1 : 0
|
91
|
+
end
|
92
|
+
|
93
|
+
protected
|
94
|
+
|
95
|
+
# Allow included modules to decorate rendered hash
|
96
|
+
#
|
97
|
+
# @param key [Symbol] the key (e.g. `embedded`, `links`)
|
98
|
+
# @param result [Hash] the partially rendered hash to decorate
|
99
|
+
#
|
100
|
+
# @return [Hash] the decorated hash
|
101
|
+
#
|
102
|
+
def decorate_render(key, result)
|
103
|
+
result.tap do
|
104
|
+
value = send(key)
|
105
|
+
|
106
|
+
result[:"_#{key}"] = value if value.any?
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Iterate through enabled definitions of the given type, allowing instance
|
111
|
+
# to build up resulting hash
|
112
|
+
#
|
113
|
+
# @param type [Symbol, String] the definition type
|
114
|
+
#
|
115
|
+
# @return [Hash] the result
|
116
|
+
#
|
117
|
+
def render_definitions(type)
|
118
|
+
definitions = self.class.definitions.fetch(type, [])
|
119
|
+
|
120
|
+
definitions.each_with_object({}) do |definition, result|
|
121
|
+
next unless definition.enabled?(self)
|
122
|
+
|
123
|
+
yield definition, result
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
class << self
|
129
|
+
# @yield [Halogen::Configuration] configuration instance for modification
|
130
|
+
#
|
131
|
+
def configure
|
132
|
+
yield config
|
133
|
+
end
|
134
|
+
|
135
|
+
# Configuration instance
|
136
|
+
#
|
137
|
+
# @return [Halogen::Configuration]
|
138
|
+
#
|
139
|
+
def config
|
140
|
+
@config ||= Configuration.new
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
require 'halogen/collection'
|
146
|
+
require 'halogen/configuration'
|
147
|
+
require 'halogen/definition'
|
148
|
+
require 'halogen/definitions'
|
149
|
+
require 'halogen/embeds'
|
150
|
+
require 'halogen/errors'
|
151
|
+
require 'halogen/links'
|
152
|
+
require 'halogen/properties'
|
153
|
+
require 'halogen/resource'
|
154
|
+
require 'halogen/hash_util'
|
155
|
+
require 'halogen/version'
|
156
|
+
|
157
|
+
require 'halogen/railtie' if defined?(::Rails)
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
module Halogen
|
4
|
+
# Behavior for representers with a primary collection resource.
|
5
|
+
#
|
6
|
+
# The main reason to declare a collection is that the resource with that name
|
7
|
+
# will always be embedded during rendering.
|
8
|
+
#
|
9
|
+
module Collection
|
10
|
+
def self.included(base) # :nodoc:
|
11
|
+
if base.included_modules.include?(Resource)
|
12
|
+
fail InvalidCollection, "#{base.name} has already defined a resource"
|
13
|
+
end
|
14
|
+
|
15
|
+
base.extend ClassMethods
|
16
|
+
|
17
|
+
base.send :include, InstanceMethods
|
18
|
+
|
19
|
+
base.class.send :attr_accessor, :collection_name
|
20
|
+
end
|
21
|
+
|
22
|
+
module ClassMethods # :nodoc:
|
23
|
+
# @param name [Symbol, String] name of the collection
|
24
|
+
#
|
25
|
+
# @return [Module] self
|
26
|
+
#
|
27
|
+
def define_collection(name)
|
28
|
+
self.collection_name = name.to_s
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
module InstanceMethods # :nodoc:
|
33
|
+
# Override super to ensure that the primary collection is always embedded
|
34
|
+
#
|
35
|
+
# @param key [String] the embed key to check
|
36
|
+
#
|
37
|
+
# @return [true, false] whether the given key should be embedded
|
38
|
+
#
|
39
|
+
def embed?(key)
|
40
|
+
super || key == self.class.collection_name
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
module Halogen
|
4
|
+
# Simple configuration class
|
5
|
+
#
|
6
|
+
class Configuration
|
7
|
+
# Array of extension modules to be included in all representers
|
8
|
+
#
|
9
|
+
# @return [Array<Module>]
|
10
|
+
#
|
11
|
+
def extensions
|
12
|
+
@extensions ||= []
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
module Halogen
|
4
|
+
# Stores instructions for how to render a value for a given representer
|
5
|
+
# instance
|
6
|
+
#
|
7
|
+
class Definition
|
8
|
+
attr_reader :name, :options
|
9
|
+
|
10
|
+
attr_accessor :procedure
|
11
|
+
|
12
|
+
# Construct a new Definition instance
|
13
|
+
#
|
14
|
+
# @param name [Symbol, String] definition name
|
15
|
+
# @param options [Hash] hash of options
|
16
|
+
#
|
17
|
+
# @return [Halogen::Definition] the instance
|
18
|
+
#
|
19
|
+
def initialize(name, options, procedure)
|
20
|
+
@name = name.to_sym
|
21
|
+
@options = Halogen::HashUtil.symbolize_keys!(options)
|
22
|
+
@procedure = procedure
|
23
|
+
end
|
24
|
+
|
25
|
+
# @param instance [Object] the representer instance with which to evaluate
|
26
|
+
# the stored procedure
|
27
|
+
#
|
28
|
+
def value(instance)
|
29
|
+
options.fetch(:value) do
|
30
|
+
procedure ? instance.instance_eval(&procedure) : instance.send(name)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# @return [true, false] whether this definition should be included based on
|
35
|
+
# its conditional guard, if any
|
36
|
+
#
|
37
|
+
def enabled?(instance)
|
38
|
+
if options.key?(:if)
|
39
|
+
!!eval_guard(instance, options.fetch(:if))
|
40
|
+
elsif options.key?(:unless)
|
41
|
+
!eval_guard(instance, options.fetch(:unless))
|
42
|
+
else
|
43
|
+
true
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# @return [true] if nothing is raised
|
48
|
+
#
|
49
|
+
# @raise [Halogen::InvalidDefinition] if the definition is invalid
|
50
|
+
#
|
51
|
+
def validate
|
52
|
+
return true unless options.key?(:value) && procedure
|
53
|
+
|
54
|
+
fail InvalidDefinition,
|
55
|
+
"Cannot specify both value and procedure for #{name}"
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
# Evaluate guard procedure or method
|
61
|
+
#
|
62
|
+
def eval_guard(instance, guard)
|
63
|
+
case guard
|
64
|
+
when Proc
|
65
|
+
instance.instance_eval(&guard)
|
66
|
+
when Symbol, String
|
67
|
+
instance.send(guard)
|
68
|
+
else
|
69
|
+
guard
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
module Halogen
|
4
|
+
# Each representer class has a Halogen::Definitions instance which stores
|
5
|
+
# Halogen::Definition instances by type.
|
6
|
+
#
|
7
|
+
class Definitions < Hash
|
8
|
+
# @param definition [Halogen::Definition]
|
9
|
+
#
|
10
|
+
# @return [Halogen::Definition] the added definition
|
11
|
+
#
|
12
|
+
def add(definition)
|
13
|
+
type = definition.class.name
|
14
|
+
|
15
|
+
definition.validate
|
16
|
+
|
17
|
+
self[type] ||= []
|
18
|
+
self[type] << definition
|
19
|
+
|
20
|
+
definition
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
module Halogen
|
4
|
+
module Embeds # :nodoc:
|
5
|
+
def self.included(base) # :nodoc:
|
6
|
+
base.extend ClassMethods
|
7
|
+
|
8
|
+
base.send :include, InstanceMethods
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods # :nodoc:
|
12
|
+
# @param name [Symbol, String]
|
13
|
+
# @param options [nil, Hash]
|
14
|
+
#
|
15
|
+
# @return [Halogen::Embeds::Definition]
|
16
|
+
#
|
17
|
+
def embed(name, options = {}, &procedure)
|
18
|
+
definitions.add(Definition.new(name, options, procedure))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module InstanceMethods # :nodoc:
|
23
|
+
# @return [Hash] the rendered hash with embedded resources, if any
|
24
|
+
#
|
25
|
+
def render
|
26
|
+
decorate_render :embedded, super
|
27
|
+
end
|
28
|
+
|
29
|
+
# @return [Hash] hash of rendered resources to embed
|
30
|
+
#
|
31
|
+
def embedded
|
32
|
+
render_definitions(Definition.name) do |definition, result|
|
33
|
+
next unless embed?(definition.name.to_s)
|
34
|
+
|
35
|
+
value = instance_eval(&definition.procedure)
|
36
|
+
|
37
|
+
child = embedded_child(definition.name.to_s, value)
|
38
|
+
|
39
|
+
result[definition.name] = child if child
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return [Hash, Array<Hash>] either a single rendered child representer
|
44
|
+
# or an array of them
|
45
|
+
#
|
46
|
+
def embedded_child(key, value)
|
47
|
+
return unless value
|
48
|
+
|
49
|
+
opts = child_embed_opts(key)
|
50
|
+
|
51
|
+
if value.is_a?(Array)
|
52
|
+
value.map { |item| render_child(item, opts) }.compact
|
53
|
+
else
|
54
|
+
render_child(value, opts)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# @param key [String]
|
59
|
+
#
|
60
|
+
# @return [Hash]
|
61
|
+
#
|
62
|
+
def child_embed_opts(key)
|
63
|
+
opts = embed_options.fetch(key, {})
|
64
|
+
|
65
|
+
# Turn { :report => 1 } into { :report => {} } for child
|
66
|
+
opts = {} unless opts.is_a?(Hash)
|
67
|
+
|
68
|
+
opts
|
69
|
+
end
|
70
|
+
|
71
|
+
# @param repr [Object] the child representer
|
72
|
+
# @param opts [Hash] the embed options to assign to the child
|
73
|
+
#
|
74
|
+
# @return [nil, Hash] the rendered child
|
75
|
+
#
|
76
|
+
def render_child(repr, opts)
|
77
|
+
return unless repr.class.included_modules.include?(Halogen)
|
78
|
+
|
79
|
+
repr.options.merge!(embed: opts, parent: self)
|
80
|
+
|
81
|
+
repr.render
|
82
|
+
end
|
83
|
+
|
84
|
+
# @return [Hash] hash of options with top level string keys
|
85
|
+
#
|
86
|
+
def embed_options
|
87
|
+
options.fetch(:embed, {}).tap do |result|
|
88
|
+
Halogen::HashUtil.stringify_keys!(result)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# @param key [String]
|
93
|
+
#
|
94
|
+
# @return [true, false] whether to embed the key
|
95
|
+
#
|
96
|
+
def embed?(key)
|
97
|
+
return false unless embed_options.include?(key)
|
98
|
+
|
99
|
+
!%w(0 false).include?(embed_options.fetch(key).to_s)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
require 'halogen/embeds/definition'
|