halogen 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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'
|