agave-client 0.1.0
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 +12 -0
- data/.travis.yml +14 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/CONTRIBUTORS.md +34 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +29 -0
- data/README.md +41 -0
- data/Rakefile +7 -0
- data/agave-client.gemspec +63 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/exe/agave +10 -0
- data/lib/agave.rb +11 -0
- data/lib/agave/api_client.rb +97 -0
- data/lib/agave/api_error.rb +23 -0
- data/lib/agave/cli.rb +93 -0
- data/lib/agave/dump/dsl/add_to_data_file.rb +14 -0
- data/lib/agave/dump/dsl/create_data_file.rb +14 -0
- data/lib/agave/dump/dsl/create_post.rb +33 -0
- data/lib/agave/dump/dsl/directory.rb +30 -0
- data/lib/agave/dump/dsl/root.rb +37 -0
- data/lib/agave/dump/format.rb +28 -0
- data/lib/agave/dump/format/json.rb +18 -0
- data/lib/agave/dump/format/toml.rb +31 -0
- data/lib/agave/dump/format/yaml.rb +55 -0
- data/lib/agave/dump/operation/add_to_data_file.rb +41 -0
- data/lib/agave/dump/operation/create_data_file.rb +28 -0
- data/lib/agave/dump/operation/create_post.rb +34 -0
- data/lib/agave/dump/operation/directory.rb +34 -0
- data/lib/agave/dump/operation/root.rb +27 -0
- data/lib/agave/dump/runner.rb +47 -0
- data/lib/agave/dump/ssg_detector.rb +36 -0
- data/lib/agave/json_api_deserializer.rb +38 -0
- data/lib/agave/json_api_serializer.rb +145 -0
- data/lib/agave/local/entities_repo.rb +46 -0
- data/lib/agave/local/field_type/boolean.rb +12 -0
- data/lib/agave/local/field_type/color.rb +63 -0
- data/lib/agave/local/field_type/date.rb +12 -0
- data/lib/agave/local/field_type/date_time.rb +12 -0
- data/lib/agave/local/field_type/file.rb +65 -0
- data/lib/agave/local/field_type/float.rb +12 -0
- data/lib/agave/local/field_type/gallery.rb +23 -0
- data/lib/agave/local/field_type/global_seo.rb +56 -0
- data/lib/agave/local/field_type/image.rb +82 -0
- data/lib/agave/local/field_type/integer.rb +12 -0
- data/lib/agave/local/field_type/json.rb +13 -0
- data/lib/agave/local/field_type/lat_lon.rb +30 -0
- data/lib/agave/local/field_type/link.rb +12 -0
- data/lib/agave/local/field_type/links.rb +21 -0
- data/lib/agave/local/field_type/rich_text.rb +21 -0
- data/lib/agave/local/field_type/seo.rb +33 -0
- data/lib/agave/local/field_type/slug.rb +12 -0
- data/lib/agave/local/field_type/string.rb +12 -0
- data/lib/agave/local/field_type/text.rb +12 -0
- data/lib/agave/local/field_type/theme.rb +43 -0
- data/lib/agave/local/field_type/video.rb +77 -0
- data/lib/agave/local/item.rb +179 -0
- data/lib/agave/local/items_repo.rb +210 -0
- data/lib/agave/local/json_api_entity.rb +78 -0
- data/lib/agave/local/loader.rb +60 -0
- data/lib/agave/local/site.rb +73 -0
- data/lib/agave/paginator.rb +33 -0
- data/lib/agave/repo.rb +93 -0
- data/lib/agave/site/client.rb +24 -0
- data/lib/agave/upload/file.rb +92 -0
- data/lib/agave/upload/image.rb +8 -0
- data/lib/agave/utils/favicon_tags_builder.rb +85 -0
- data/lib/agave/utils/locale_value.rb +15 -0
- data/lib/agave/utils/meta_tags/article_modified_time.rb +15 -0
- data/lib/agave/utils/meta_tags/article_publisher.rb +18 -0
- data/lib/agave/utils/meta_tags/base.rb +55 -0
- data/lib/agave/utils/meta_tags/description.rb +24 -0
- data/lib/agave/utils/meta_tags/image.rb +35 -0
- data/lib/agave/utils/meta_tags/og_locale.rb +15 -0
- data/lib/agave/utils/meta_tags/og_site_name.rb +18 -0
- data/lib/agave/utils/meta_tags/og_type.rb +18 -0
- data/lib/agave/utils/meta_tags/robots.rb +14 -0
- data/lib/agave/utils/meta_tags/title.rb +46 -0
- data/lib/agave/utils/meta_tags/twitter_card.rb +14 -0
- data/lib/agave/utils/meta_tags/twitter_site.rb +18 -0
- data/lib/agave/utils/seo_tags_builder.rb +45 -0
- data/lib/agave/version.rb +4 -0
- data/lib/agave/watch/site_change_watcher.rb +37 -0
- metadata +504 -0
@@ -0,0 +1,43 @@
|
|
1
|
+
module Agave
|
2
|
+
module Local
|
3
|
+
module FieldType
|
4
|
+
class Theme
|
5
|
+
attr_reader :primary_color, :dark_color, :light_color, :accent_color
|
6
|
+
|
7
|
+
def self.parse(value, repo)
|
8
|
+
value && new(
|
9
|
+
value[:logo],
|
10
|
+
value[:primary_color],
|
11
|
+
value[:dark_color],
|
12
|
+
value[:light_color],
|
13
|
+
value[:accent_color],
|
14
|
+
repo
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(logo, primary_color, dark_color, light_color, accent_color, repo)
|
19
|
+
@logo = logo
|
20
|
+
@primary_color = primary_color
|
21
|
+
@dark_color = dark_color
|
22
|
+
@light_color = light_color
|
23
|
+
@accent_color = accent_color
|
24
|
+
@repo = repo
|
25
|
+
end
|
26
|
+
|
27
|
+
def logo
|
28
|
+
@logo && File.parse(@logo, @repo)
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_hash(*args)
|
32
|
+
{
|
33
|
+
primary_color: primary_color,
|
34
|
+
dark_color: dark_color,
|
35
|
+
light_color: light_color,
|
36
|
+
accent_color: accent_color,
|
37
|
+
logo: logo && logo.to_hash(*args)
|
38
|
+
}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'active_support/core_ext/hash/compact'
|
3
|
+
|
4
|
+
module Agave
|
5
|
+
module Local
|
6
|
+
module FieldType
|
7
|
+
class Video
|
8
|
+
attr_reader :url
|
9
|
+
attr_reader :thumbnail_url
|
10
|
+
attr_reader :title
|
11
|
+
attr_reader :width
|
12
|
+
attr_reader :height
|
13
|
+
attr_reader :provider
|
14
|
+
attr_reader :provider_url
|
15
|
+
attr_reader :provider_uid
|
16
|
+
|
17
|
+
def self.parse(value, _repo)
|
18
|
+
value && new(
|
19
|
+
value[:url],
|
20
|
+
value[:thumbnail_url],
|
21
|
+
value[:title],
|
22
|
+
value[:width],
|
23
|
+
value[:height],
|
24
|
+
value[:provider],
|
25
|
+
value[:provider_url],
|
26
|
+
value[:provider_uid]
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(
|
31
|
+
url,
|
32
|
+
thumbnail_url,
|
33
|
+
title,
|
34
|
+
width,
|
35
|
+
height,
|
36
|
+
provider,
|
37
|
+
provider_url,
|
38
|
+
provider_uid
|
39
|
+
)
|
40
|
+
@url = url
|
41
|
+
@thumbnail_url = thumbnail_url
|
42
|
+
@title = title
|
43
|
+
@width = width
|
44
|
+
@height = height
|
45
|
+
@provider = provider
|
46
|
+
@provider_url = provider_url
|
47
|
+
@provider_uid = provider_uid
|
48
|
+
end
|
49
|
+
|
50
|
+
def iframe_embed(width = self.width, height = self.height)
|
51
|
+
# rubocop:disable Metrics/LineLength
|
52
|
+
if provider == 'youtube'
|
53
|
+
%(<iframe width="#{width}" height="#{height}" src="//www.youtube.com/embed/#{provider_uid}?rel=0" frameborder="0" allowfullscreen></iframe>)
|
54
|
+
elsif provider == 'vimeo'
|
55
|
+
%(<iframe src="//player.vimeo.com/video/#{provider_uid}?title=0&byline=0&portrait=0" width="#{width}" height="#{height}" frameborder="0" webkitAllowFullScreen mozallowfullscreen allowFullScreen></iframe>)
|
56
|
+
elsif provider == 'facebook'
|
57
|
+
%(<iframe src="//www.facebook.com/plugins/video.php?href=#{url}&width=#{width}&show_text=false&height=#{height}" width="#{width}" height="#{height}" style="border:none;overflow:hidden;width:100%;" scrolling="no" frameborder="0" allowTransparency="true" allow="encrypted-media" allowFullScreen="true"></iframe>)
|
58
|
+
end
|
59
|
+
# rubocop:enable Metrics/LineLength
|
60
|
+
end
|
61
|
+
|
62
|
+
def to_hash(*_args)
|
63
|
+
{
|
64
|
+
url: url,
|
65
|
+
thumbnail_url: thumbnail_url,
|
66
|
+
title: title,
|
67
|
+
width: width,
|
68
|
+
height: height,
|
69
|
+
provider: provider,
|
70
|
+
provider_url: provider_url,
|
71
|
+
provider_uid: provider_uid
|
72
|
+
}
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'forwardable'
|
3
|
+
require 'active_support/inflector/transliterate'
|
4
|
+
require 'active_support/hash_with_indifferent_access'
|
5
|
+
require 'agave/utils/locale_value'
|
6
|
+
|
7
|
+
Dir[File.dirname(__FILE__) + '/field_type/*.rb'].each do |file|
|
8
|
+
require file
|
9
|
+
end
|
10
|
+
|
11
|
+
module Agave
|
12
|
+
module Local
|
13
|
+
class Item
|
14
|
+
extend Forwardable
|
15
|
+
|
16
|
+
attr_reader :entity
|
17
|
+
def_delegators :entity, :id
|
18
|
+
|
19
|
+
def initialize(entity, items_repo)
|
20
|
+
@entity = entity
|
21
|
+
@items_repo = items_repo
|
22
|
+
end
|
23
|
+
|
24
|
+
def ==(other)
|
25
|
+
other.is_a?(Item) && other.id == id
|
26
|
+
end
|
27
|
+
|
28
|
+
def seo_meta_tags
|
29
|
+
Utils::SeoTagsBuilder.new(self, @items_repo.site).meta_tags
|
30
|
+
end
|
31
|
+
|
32
|
+
def singleton?
|
33
|
+
item_type.singleton
|
34
|
+
end
|
35
|
+
alias single_instance? singleton?
|
36
|
+
|
37
|
+
def item_type
|
38
|
+
@item_type ||= entity.item_type
|
39
|
+
end
|
40
|
+
|
41
|
+
def fields
|
42
|
+
@fields ||= item_type.fields.sort_by(&:position)
|
43
|
+
end
|
44
|
+
|
45
|
+
def attributes
|
46
|
+
fields.each_with_object(
|
47
|
+
ActiveSupport::HashWithIndifferentAccess.new
|
48
|
+
) do |field, acc|
|
49
|
+
acc[field.api_key.to_sym] = send(field.api_key)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def position
|
54
|
+
entity.position
|
55
|
+
end
|
56
|
+
|
57
|
+
def parent
|
58
|
+
@items_repo.find(entity.parent_id) if item_type.tree && entity.parent_id
|
59
|
+
end
|
60
|
+
|
61
|
+
def children
|
62
|
+
@items_repo.children_of(id).sort_by(&:position) if item_type.tree
|
63
|
+
end
|
64
|
+
|
65
|
+
def updated_at
|
66
|
+
Time.parse(entity.updated_at).utc
|
67
|
+
end
|
68
|
+
|
69
|
+
def created_at
|
70
|
+
# @TODO ?
|
71
|
+
# Time.parse(entity.created_at).utc
|
72
|
+
end
|
73
|
+
|
74
|
+
def to_s
|
75
|
+
api_key = item_type.api_key
|
76
|
+
"#<Item id=#{id} item_type=#{api_key} attributes=#{attributes}>"
|
77
|
+
end
|
78
|
+
alias inspect to_s
|
79
|
+
|
80
|
+
def to_hash(max_depth = 3, current_depth = 0)
|
81
|
+
return id if current_depth >= max_depth
|
82
|
+
|
83
|
+
base = {
|
84
|
+
id: id,
|
85
|
+
item_type: item_type.api_key,
|
86
|
+
updated_at: updated_at,
|
87
|
+
created_at: created_at
|
88
|
+
}
|
89
|
+
|
90
|
+
base[:position] = position if item_type.sortable
|
91
|
+
|
92
|
+
if item_type.tree
|
93
|
+
base[:position] = position
|
94
|
+
base[:children] = children.map do |child|
|
95
|
+
child.to_hash(
|
96
|
+
max_depth,
|
97
|
+
current_depth + 1
|
98
|
+
)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
fields.each_with_object(base) do |field, result|
|
103
|
+
value = send(field.api_key)
|
104
|
+
|
105
|
+
result[field.api_key.to_sym] =
|
106
|
+
if value.respond_to?(:to_hash)
|
107
|
+
m = value.method(:to_hash)
|
108
|
+
if m.arity == 2
|
109
|
+
value.to_hash(max_depth, current_depth + 1)
|
110
|
+
else
|
111
|
+
value.to_hash
|
112
|
+
end
|
113
|
+
else
|
114
|
+
value
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def read_attribute(method, field)
|
122
|
+
field_type = field.field_type
|
123
|
+
type_klass_name = "::Agave::Local::FieldType::#{field_type.camelize}"
|
124
|
+
type_klass = type_klass_name.safe_constantize
|
125
|
+
|
126
|
+
value = if field.localized
|
127
|
+
obj = entity.send(method) || {}
|
128
|
+
Utils::LocaleValue.find(obj)
|
129
|
+
else
|
130
|
+
entity.send(method)
|
131
|
+
end
|
132
|
+
|
133
|
+
if type_klass
|
134
|
+
type_klass.parse(value, @items_repo)
|
135
|
+
else
|
136
|
+
warning = [
|
137
|
+
"Warning: unrecognized field of type `#{field_type}`",
|
138
|
+
"for item `#{item_type.api_key}` and",
|
139
|
+
"field `#{method}`: returning a simple Hash instead.",
|
140
|
+
'Please upgrade to the latest version of the `agave` gem!'
|
141
|
+
]
|
142
|
+
puts warning.join(' ')
|
143
|
+
|
144
|
+
value
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def method_missing(method, *arguments, &block)
|
149
|
+
field = fields.find { |f| f.api_key.to_sym == method }
|
150
|
+
if field && arguments.empty?
|
151
|
+
read_attribute(method, field)
|
152
|
+
else
|
153
|
+
super
|
154
|
+
end
|
155
|
+
rescue NoMethodError => e
|
156
|
+
if e.name === method
|
157
|
+
message = []
|
158
|
+
message << "Undefined method `#{method}`"
|
159
|
+
message << "Available fields for a `#{item_type.api_key}` item:"
|
160
|
+
message += fields.map do |f|
|
161
|
+
"* .#{f.api_key}"
|
162
|
+
end
|
163
|
+
raise NoMethodError, message.join("\n")
|
164
|
+
else
|
165
|
+
raise e
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def respond_to_missing?(method, include_private = false)
|
170
|
+
field = fields.find { |f| f.api_key.to_sym == method }
|
171
|
+
if field
|
172
|
+
true
|
173
|
+
else
|
174
|
+
super
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
@@ -0,0 +1,210 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'active_support/core_ext/string'
|
3
|
+
require 'agave/local/item'
|
4
|
+
require 'agave/local/site'
|
5
|
+
|
6
|
+
module Agave
|
7
|
+
module Local
|
8
|
+
class ItemsRepo
|
9
|
+
attr_reader :entities_repo, :collections_by_type, :item_type_methods
|
10
|
+
|
11
|
+
def initialize(entities_repo)
|
12
|
+
@entities_repo = entities_repo
|
13
|
+
@collections_by_type = {}
|
14
|
+
@items_by_id = {}
|
15
|
+
@items_by_parent_id = {}
|
16
|
+
@item_type_methods = {}
|
17
|
+
|
18
|
+
build_cache!
|
19
|
+
end
|
20
|
+
|
21
|
+
def find(id)
|
22
|
+
@items_by_id[id.to_s]
|
23
|
+
end
|
24
|
+
|
25
|
+
def children_of(id)
|
26
|
+
@items_by_parent_id.fetch(id.to_s, [])
|
27
|
+
end
|
28
|
+
|
29
|
+
def respond_to_missing?(method, include_private = false)
|
30
|
+
if collections_by_type.key?(method)
|
31
|
+
true
|
32
|
+
else
|
33
|
+
super
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def site
|
38
|
+
Site.new(
|
39
|
+
entities_repo.find_entities_of_type('site').first,
|
40
|
+
self
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
def available_locales
|
45
|
+
site.locales.map(&:to_sym)
|
46
|
+
end
|
47
|
+
|
48
|
+
def item_types
|
49
|
+
entities_repo.find_entities_of_type('item_type')
|
50
|
+
end
|
51
|
+
|
52
|
+
def single_instance_item_types
|
53
|
+
item_types.select(&:singleton)
|
54
|
+
end
|
55
|
+
|
56
|
+
def collection_item_types
|
57
|
+
item_types.select do |item_type|
|
58
|
+
!item_type.singleton
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def items_of_type(item_type)
|
63
|
+
method = item_type_methods[item_type]
|
64
|
+
|
65
|
+
if item_type.singleton
|
66
|
+
Array(@collections_by_type[method])
|
67
|
+
else
|
68
|
+
@collections_by_type[method]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def build_cache!
|
75
|
+
build_item_type_methods!
|
76
|
+
build_collections_by_type!
|
77
|
+
build_singletons_by_type!
|
78
|
+
end
|
79
|
+
|
80
|
+
def build_item_type_methods!
|
81
|
+
@item_type_methods = {}
|
82
|
+
|
83
|
+
singleton_keys = single_instance_item_types.map(&:api_key)
|
84
|
+
collection_keys = collection_item_types.map(&:api_key)
|
85
|
+
.map(&:pluralize)
|
86
|
+
|
87
|
+
clashing_keys = singleton_keys & collection_keys
|
88
|
+
|
89
|
+
item_types.each do |item_type|
|
90
|
+
pluralized_api_key = item_type.api_key.pluralize
|
91
|
+
|
92
|
+
method = if item_type.singleton
|
93
|
+
item_type.api_key
|
94
|
+
else
|
95
|
+
pluralized_api_key
|
96
|
+
end
|
97
|
+
|
98
|
+
if clashing_keys.include?(pluralized_api_key)
|
99
|
+
suffix = item_type.singleton ? 'instance' : 'collection'
|
100
|
+
method = "#{method}_#{suffix}"
|
101
|
+
end
|
102
|
+
|
103
|
+
@item_type_methods[item_type] = method.to_sym
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def build_collections_by_type!
|
108
|
+
item_types.each do |item_type|
|
109
|
+
method = item_type_methods[item_type]
|
110
|
+
@collections_by_type[method] = if item_type.singleton
|
111
|
+
nil
|
112
|
+
else
|
113
|
+
ItemCollection.new
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
item_entities.each do |item_entity|
|
118
|
+
item = Item.new(item_entity, self)
|
119
|
+
method = item_type_methods[item_entity.item_type]
|
120
|
+
|
121
|
+
unless item_entity.item_type.singleton
|
122
|
+
@collections_by_type[method].push item
|
123
|
+
end
|
124
|
+
|
125
|
+
@items_by_id[item.id] = item
|
126
|
+
|
127
|
+
if item_entity.respond_to?(:parent_id) && item_entity.parent_id
|
128
|
+
@items_by_parent_id[item_entity.parent_id] ||= []
|
129
|
+
@items_by_parent_id[item_entity.parent_id] << item
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
item_types.each do |item_type|
|
134
|
+
method = item_type_methods[item_type]
|
135
|
+
if !item_type.singleton && item_type.sortable
|
136
|
+
@collections_by_type[method].sort_by!(&:position)
|
137
|
+
elsif item_type.ordering_field
|
138
|
+
@collections_by_type[method].sort_by! do |item|
|
139
|
+
item.send(item_type.ordering_field.api_key)
|
140
|
+
end
|
141
|
+
if item_type.ordering_direction == 'desc'
|
142
|
+
@collections_by_type[method].reverse!
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def build_singletons_by_type!
|
149
|
+
item_types.each do |item_type|
|
150
|
+
method = item_type_methods[item_type]
|
151
|
+
next unless item_type.singleton
|
152
|
+
|
153
|
+
item = if item_type.singleton_item
|
154
|
+
@items_by_id[item_type.singleton_item.id]
|
155
|
+
end
|
156
|
+
|
157
|
+
@collections_by_type[method] = item
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def item_entities
|
162
|
+
entities_repo.find_entities_of_type('item')
|
163
|
+
end
|
164
|
+
|
165
|
+
def method_missing(method, *arguments, &block)
|
166
|
+
if collections_by_type.key?(method) && arguments.empty?
|
167
|
+
collections_by_type[method]
|
168
|
+
else
|
169
|
+
super
|
170
|
+
end
|
171
|
+
rescue NoMethodError
|
172
|
+
message = []
|
173
|
+
message << "Undefined method `#{method}`"
|
174
|
+
message << 'Available AgaveCMS collections/items:'
|
175
|
+
message += collections_by_type.map do |key, _value|
|
176
|
+
"* .#{key}"
|
177
|
+
end
|
178
|
+
raise NoMethodError, message.join("\n")
|
179
|
+
end
|
180
|
+
|
181
|
+
class ItemCollection < Array
|
182
|
+
def each(&block)
|
183
|
+
if block && block.arity == 2
|
184
|
+
each_with_object({}) do |item, acc|
|
185
|
+
acc[item.id] = item
|
186
|
+
end.each(&block)
|
187
|
+
else
|
188
|
+
super(&block)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def [](id)
|
193
|
+
if id.is_a? String
|
194
|
+
find { |item| item.id == id }
|
195
|
+
else
|
196
|
+
super(id)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def keys
|
201
|
+
map(&:id)
|
202
|
+
end
|
203
|
+
|
204
|
+
def values
|
205
|
+
to_a
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|