pack_api 1.0.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/CHANGELOG.md +39 -0
- data/LICENSE.txt +21 -0
- data/README.md +238 -0
- data/lib/pack_api/config/dry_types_initializer.rb +1 -0
- data/lib/pack_api/models/internal_error.rb +25 -0
- data/lib/pack_api/models/mapping/abstract_transformer.rb +46 -0
- data/lib/pack_api/models/mapping/api_to_model_attributes_transformer.rb +27 -0
- data/lib/pack_api/models/mapping/attribute_hash_transformer.rb +46 -0
- data/lib/pack_api/models/mapping/attribute_map.rb +268 -0
- data/lib/pack_api/models/mapping/attribute_map_registry.rb +21 -0
- data/lib/pack_api/models/mapping/error_hash_to_api_attributes_transformer.rb +101 -0
- data/lib/pack_api/models/mapping/filter_map.rb +97 -0
- data/lib/pack_api/models/mapping/model_to_api_attributes_transformer.rb +67 -0
- data/lib/pack_api/models/mapping/normalized_api_attribute.rb +40 -0
- data/lib/pack_api/models/mapping/null_transformer.rb +9 -0
- data/lib/pack_api/models/mapping/value_object_factory.rb +83 -0
- data/lib/pack_api/models/pagination/opaque_token_v2.rb +19 -0
- data/lib/pack_api/models/pagination/paginator.rb +155 -0
- data/lib/pack_api/models/pagination/paginator_builder.rb +112 -0
- data/lib/pack_api/models/pagination/paginator_cursor.rb +86 -0
- data/lib/pack_api/models/pagination/snapshot_paginator.rb +133 -0
- data/lib/pack_api/models/querying/abstract_boolean_filter.rb +38 -0
- data/lib/pack_api/models/querying/abstract_enum_filter.rb +54 -0
- data/lib/pack_api/models/querying/abstract_filter.rb +15 -0
- data/lib/pack_api/models/querying/abstract_numeric_filter.rb +37 -0
- data/lib/pack_api/models/querying/abstract_range_filter.rb +31 -0
- data/lib/pack_api/models/querying/attribute_filter.rb +36 -0
- data/lib/pack_api/models/querying/attribute_filter_factory.rb +62 -0
- data/lib/pack_api/models/querying/collection_query.rb +125 -0
- data/lib/pack_api/models/querying/composable_query.rb +22 -0
- data/lib/pack_api/models/querying/default_filter.rb +20 -0
- data/lib/pack_api/models/querying/discoverable_filter.rb +33 -0
- data/lib/pack_api/models/querying/dynamic_enum_filter.rb +20 -0
- data/lib/pack_api/models/querying/filter_factory.rb +54 -0
- data/lib/pack_api/models/querying/sort_hash.rb +36 -0
- data/lib/pack_api/models/types/aggregate_type.rb +202 -0
- data/lib/pack_api/models/types/base_type.rb +46 -0
- data/lib/pack_api/models/types/boolean_filter_definition.rb +9 -0
- data/lib/pack_api/models/types/collection_result_metadata.rb +48 -0
- data/lib/pack_api/models/types/custom_filter_definition.rb +8 -0
- data/lib/pack_api/models/types/enum_filter_definition.rb +10 -0
- data/lib/pack_api/models/types/filter_option.rb +8 -0
- data/lib/pack_api/models/types/globally_identifiable.rb +19 -0
- data/lib/pack_api/models/types/numeric_filter_definition.rb +9 -0
- data/lib/pack_api/models/types/range_filter_definition.rb +10 -0
- data/lib/pack_api/models/types/result.rb +70 -0
- data/lib/pack_api/models/types/simple_filter_definition.rb +9 -0
- data/lib/pack_api/models/values_in_background_batches.rb +58 -0
- data/lib/pack_api/models/values_in_batches.rb +51 -0
- data/lib/pack_api/version.rb +5 -0
- data/lib/pack_api.rb +72 -0
- data/lib/types.rb +3 -0
- metadata +276 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 97d36561d79762a67e842fc092ff22d1d66de8c230e1abcb6adff3bedc86edf7
|
|
4
|
+
data.tar.gz: e4145510ed3cb11d94340c8b48d21dd454037dd8906ac00004f5bbbdc0b43d55
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: e0f38e85af2248f3898ebbf99fa534133fbc5c2e10c6e8958aa7919b75174150a1ad005f4a31ed267c1674d56f02c1ad0e58c484db523f83e486c695a38e0207
|
|
7
|
+
data.tar.gz: 1c5ab5ad08af202d504550d3266c539a65836f210e434caa9330dee911b2285f6fabb2b9d4507ff3bbcab33976f67f5b8ecd226c3f31f115222acac64aba0435
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2025-11-10
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Initial release of PackAPI gem
|
|
15
|
+
- Mapping module for transforming data between domain models and API representations
|
|
16
|
+
- AttributeMap for defining bidirectional mappings
|
|
17
|
+
- AttributeMapRegistry for centralized mapping management
|
|
18
|
+
- ModelToAPIAttributesTransformer and APIToModelAttributesTransformer
|
|
19
|
+
- ValueObjectFactory for creating value objects
|
|
20
|
+
- Querying module for building flexible query interfaces
|
|
21
|
+
- ComposableQuery and CollectionQuery
|
|
22
|
+
- Filter implementations (boolean, enum, numeric, range)
|
|
23
|
+
- FilterFactory for dynamic filter creation
|
|
24
|
+
- SortHash for handling sorting parameters
|
|
25
|
+
- Pagination module with multiple strategies
|
|
26
|
+
- Paginator for standard pagination
|
|
27
|
+
- SnapshotPaginator for consistent results
|
|
28
|
+
- PaginatorBuilder for custom configurations
|
|
29
|
+
- OpaqueTokenV2 for secure cursor tokens
|
|
30
|
+
- Types module with dry-types integration
|
|
31
|
+
- BaseType and AggregateType
|
|
32
|
+
- Result and CollectionResultMetadata
|
|
33
|
+
- Filter definition types
|
|
34
|
+
- Batch operation utilities
|
|
35
|
+
- ValuesInBatches for synchronous batch processing
|
|
36
|
+
- ValuesInBackgroundBatches for asynchronous batch processing
|
|
37
|
+
|
|
38
|
+
[Unreleased]: https://github.com/flytedesk/pack_api/compare/v0.1.0...HEAD
|
|
39
|
+
[0.1.0]: https://github.com/flytedesk/pack_api/releases/tag/v0.1.0
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Flytedesk
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# PackAPI
|
|
2
|
+
|
|
3
|
+
Building blocks for implementing APIs around domain models.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
PackAPI provides a comprehensive set of tools for building robust API layers on top of domain models. It includes utilities for:
|
|
8
|
+
|
|
9
|
+
- **Data transformation** - Elements for passing data out of the API
|
|
10
|
+
- **Filter definitions** - Elements for describing the filters supported by query endpoints in the API
|
|
11
|
+
- **Attribute mapping** - Elements for building the mapping between domain models and API models
|
|
12
|
+
- **Query building** - Elements for building query endpoints based on user inputs (sort, filter, pagination)
|
|
13
|
+
- **Batch operations** - Elements for retrieving multiple pages of data from other query endpoints
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
Add this line to your application's Gemfile:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
gem 'pack_api'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
And then execute:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
bundle install
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or install it yourself as:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
gem install pack_api
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Requirements
|
|
36
|
+
|
|
37
|
+
- Ruby >= 3.0.0
|
|
38
|
+
- ActiveRecord >= 7.0
|
|
39
|
+
- dry-types ~> 1.8
|
|
40
|
+
|
|
41
|
+
## Features
|
|
42
|
+
|
|
43
|
+
### Mapping
|
|
44
|
+
|
|
45
|
+
The mapping module provides tools for transforming data between domain models and API representations:
|
|
46
|
+
|
|
47
|
+
- `AttributeMap` - Define bidirectional mappings between model and API attributes
|
|
48
|
+
- `AttributeMapRegistry` - Centralized registry for attribute mappings
|
|
49
|
+
- `ModelToAPIAttributesTransformer` - Transform model attributes to API format
|
|
50
|
+
- `APIToModelAttributesTransformer` - Transform API attributes to model format
|
|
51
|
+
- `ValueObjectFactory` - Create value objects from raw data
|
|
52
|
+
|
|
53
|
+
### Querying
|
|
54
|
+
|
|
55
|
+
Build flexible query interfaces with support for filtering, sorting, and pagination:
|
|
56
|
+
|
|
57
|
+
- `ComposableQuery` - Build complex queries from simpler components
|
|
58
|
+
- `CollectionQuery` - Query collections with filtering and sorting
|
|
59
|
+
- `AbstractFilter` - Base class for custom filters
|
|
60
|
+
- Filter implementations for boolean, enum, numeric, and range filters
|
|
61
|
+
- `FilterFactory` - Create filters dynamically based on query method arguments
|
|
62
|
+
- `SortHash` - Handle sorting parameters
|
|
63
|
+
|
|
64
|
+
### Pagination
|
|
65
|
+
|
|
66
|
+
Enable paginated access to resources across the API:
|
|
67
|
+
|
|
68
|
+
- `Paginator` - Standard pagination implementation
|
|
69
|
+
- `PaginatorBuilder` - Build paginators with custom configurations
|
|
70
|
+
- `SnapshotPaginator` - Enable record iteration (one by one) across results in a page,
|
|
71
|
+
even when the underlying records change state (and may no longer be at the same position in the result set)
|
|
72
|
+
|
|
73
|
+
### Types
|
|
74
|
+
|
|
75
|
+
Type definitions and validation using dry-types:
|
|
76
|
+
|
|
77
|
+
- `BaseType` - Base type for API models
|
|
78
|
+
- `CollectionResultMetadata` - Metadata for paginated collections
|
|
79
|
+
- `Result` - Generic result type
|
|
80
|
+
- `AggregateType` - Composite types made of attributes from other types
|
|
81
|
+
- Filter definition types for various data types
|
|
82
|
+
|
|
83
|
+
### Batch Operations
|
|
84
|
+
|
|
85
|
+
Utilities for processing large datasets efficiently:
|
|
86
|
+
|
|
87
|
+
- `ValuesInBatches` - Process values in batches
|
|
88
|
+
- `ValuesInBackgroundBatches` - Process values in background batches
|
|
89
|
+
|
|
90
|
+
## Usage
|
|
91
|
+
|
|
92
|
+
### Basic Example
|
|
93
|
+
|
|
94
|
+
See the test files for more detailed examples, but here's a simple usage example.
|
|
95
|
+
|
|
96
|
+
Let's assume your system has Author, Comment and BlogPost ActiveRecord models.
|
|
97
|
+
|
|
98
|
+
1. Define value objects to contain the data passed out of the API:
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
|
|
102
|
+
# public/author_type.rb
|
|
103
|
+
class AuthorType < PackAPI::Types::BaseType
|
|
104
|
+
attribute :id, ::Types::String
|
|
105
|
+
attribute :name, ::Types::String
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# public/comment_type.rb
|
|
109
|
+
class CommentType < PackAPI::Types::BaseType
|
|
110
|
+
attribute :text, ::Types::String
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# public/blog_post_type.rb
|
|
114
|
+
class BlogPostType < PackAPI::Types::BaseType
|
|
115
|
+
attribute :id, ::Types::String
|
|
116
|
+
attribute :legacy_id, ::Types::String
|
|
117
|
+
attribute :title, ::Types::String
|
|
118
|
+
attribute :persisted, ::Types::Bool
|
|
119
|
+
attribute :contents, ::Types::String.optional
|
|
120
|
+
optional_attribute :associated, AuthorType
|
|
121
|
+
optional_attribute :notes, ::Types::Array.of(CommentType)
|
|
122
|
+
optional_attribute :earnings_float, ::Types::Coercible::Float
|
|
123
|
+
end
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
2. Define the rules for mapping between the domain models and the API value objects:
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
# api/author_attribute_map.rb
|
|
130
|
+
class AuthorAttributeMap < PackAPI::Mapping::AttributeMap
|
|
131
|
+
api_type AuthorType
|
|
132
|
+
model_type Author
|
|
133
|
+
map :name, to: :name
|
|
134
|
+
map :id, to: :external_id
|
|
135
|
+
map :blog_posts
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# api/comment_attribute_map.rb
|
|
139
|
+
class CommentAttributeMap < PackAPI::Mapping::AttributeMap
|
|
140
|
+
api_type CommentType
|
|
141
|
+
model_type Comment
|
|
142
|
+
map :text, to: :txt
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# api/blog_post_attribute_map.rb
|
|
146
|
+
class BlogPostAttributeMap < PackAPI::Mapping::AttributeMap
|
|
147
|
+
api_type BlogPostType
|
|
148
|
+
model_type BlogPost
|
|
149
|
+
|
|
150
|
+
# example API attribute mapped to a model attribute of the same name
|
|
151
|
+
map :title
|
|
152
|
+
|
|
153
|
+
map :contents, from_model_attribute: ->(attachment) { attachment&.blob }
|
|
154
|
+
|
|
155
|
+
# example API attribute mapped to a model attribute of a different name
|
|
156
|
+
map :id, to: :external_id
|
|
157
|
+
|
|
158
|
+
# example of API attribute ending in "_id"
|
|
159
|
+
map :legacy_id
|
|
160
|
+
|
|
161
|
+
# example of API attribute mapped to a model method (unidirectional)
|
|
162
|
+
map :persisted, to: :persisted?, readonly: true
|
|
163
|
+
|
|
164
|
+
# example of API association mapped to a model association
|
|
165
|
+
# (the association_id can also be passed in, and reported on during error cases)
|
|
166
|
+
map :associated, to: :author,
|
|
167
|
+
from_api_attribute: ->(author_id) { Author.find_by(external_id: author_id) }
|
|
168
|
+
|
|
169
|
+
map :notes, to: :comments, transform_nested_attributes_with: CommentAttributeMap
|
|
170
|
+
|
|
171
|
+
# example of OPTIONAL API attribute (association) mapped to a model method (bidirectional)
|
|
172
|
+
map :earnings_float, to: :earnings_float
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
3. Implement a query endpoint using the attribute map:
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
def query_blog_posts(cursor = nil, search = nil, sort = nil, page_size = 50, filters = {}, optional_attributes = [])
|
|
181
|
+
collection = BlogPost.all
|
|
182
|
+
|
|
183
|
+
# avoid N+1 queries for optional attributes that are associations
|
|
184
|
+
if optional_attributes.include?(:associated)
|
|
185
|
+
collection = collection.includes(:author)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# convert the search terms to something used by the CollectionQuery to perform searches (hash of model attributes to search terms)
|
|
189
|
+
if search.present?
|
|
190
|
+
# search through blog post title and comments
|
|
191
|
+
collection = collection.includes(:comments)
|
|
192
|
+
model_search = {
|
|
193
|
+
'title' => search,
|
|
194
|
+
"#{Comment.table_name}.txt" => search,
|
|
195
|
+
}
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# convert the API sort to model sort
|
|
199
|
+
model_sort = BlogPostAttributeMap.model_attribute_keys(PackAPI::Querying::SortHash.new(sort))
|
|
200
|
+
|
|
201
|
+
# convert the API filters to model filters
|
|
202
|
+
model_filters = BlogPostFilterMap.new.from_api_filters(filters)
|
|
203
|
+
|
|
204
|
+
# build and execute the query
|
|
205
|
+
query = PackAPI::Querying::CollectionQuery.new(collection:)
|
|
206
|
+
query.filter_factory = Filters::BlogPost::FilterFactory.new
|
|
207
|
+
query.call(cursor:, per_page: page_size, sort: model_sort, search: model_search, filters: model_filters)
|
|
208
|
+
|
|
209
|
+
# build and return the result
|
|
210
|
+
PackAPI::Types::Result.from_collection(models: query.results,
|
|
211
|
+
value_object_factory: ValueObjectFactory.new,
|
|
212
|
+
optional_attributes:,
|
|
213
|
+
sort: BlogPostAttributeMap.api_attribute_keys(query.sort),
|
|
214
|
+
paginator: query.paginator)
|
|
215
|
+
end
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Development
|
|
219
|
+
|
|
220
|
+
After checking out the repo, run:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
bundle install
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Run the test suite:
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
bundle exec rspec
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Contributing
|
|
233
|
+
|
|
234
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/flytedesk/pack_api.
|
|
235
|
+
|
|
236
|
+
## License
|
|
237
|
+
|
|
238
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require_relative '../../types'
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PackAPI
|
|
4
|
+
###
|
|
5
|
+
# Error class for passing errors from the model to the API logic
|
|
6
|
+
class InternalError < StandardError
|
|
7
|
+
attr_reader :object, :options
|
|
8
|
+
|
|
9
|
+
def initialize(msg = nil, object: nil, options: {})
|
|
10
|
+
super(msg)
|
|
11
|
+
@object = object
|
|
12
|
+
@options = options
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def message
|
|
16
|
+
to_s
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_s
|
|
20
|
+
return super unless object.present?
|
|
21
|
+
|
|
22
|
+
"#{super} - #{object.inspect}"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PackAPI::Mapping
|
|
4
|
+
class AbstractTransformer
|
|
5
|
+
attr_accessor :mappings, :api_type, :model_type, :data_source
|
|
6
|
+
attr_reader :options
|
|
7
|
+
|
|
8
|
+
def initialize(config)
|
|
9
|
+
@mappings = config[:mappings]
|
|
10
|
+
@api_type = config[:api_type]
|
|
11
|
+
@model_type = config[:model_type]
|
|
12
|
+
@transform_value = config[:transform_value]
|
|
13
|
+
@options = {}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
###
|
|
17
|
+
# @abstract
|
|
18
|
+
def execute
|
|
19
|
+
raise NotImplementedError
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def options=(value)
|
|
23
|
+
@options = value.presence || {}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
protected
|
|
27
|
+
|
|
28
|
+
def transform_value(api_attribute, value)
|
|
29
|
+
@transform_value.call(api_attribute, value)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def model_attribute(api_attribute)
|
|
33
|
+
return api_attribute if api_attribute.start_with?('_')
|
|
34
|
+
|
|
35
|
+
unless mappings.key?(api_attribute)
|
|
36
|
+
raise ActiveModel::UnknownAttributeError.new(@model_type.name, api_attribute)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
mappings[api_attribute]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def api_attribute_names
|
|
43
|
+
api_type.attribute_names
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PackAPI::Mapping
|
|
4
|
+
###
|
|
5
|
+
# Specialized attribute transformer allowing API attributes be converted to the attribute names needed to
|
|
6
|
+
# creating/updating an ActiveRecord model.
|
|
7
|
+
class APIToModelAttributesTransformer < AbstractTransformer
|
|
8
|
+
|
|
9
|
+
def execute
|
|
10
|
+
result = {}
|
|
11
|
+
attribute_names = NormalizedAPIAttribute.new(api_attribute_names)
|
|
12
|
+
data_source.each do |api_attribute, api_value|
|
|
13
|
+
normalized_api_attribute = attribute_names.normalize(api_attribute)
|
|
14
|
+
model_attribute = model_attribute(normalized_api_attribute)
|
|
15
|
+
model_value = model_value(normalized_api_attribute, api_value)
|
|
16
|
+
result.deep_merge!({ model_attribute => model_value })
|
|
17
|
+
end
|
|
18
|
+
result
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def model_value(api_attribute, api_value)
|
|
24
|
+
transform_value(api_attribute, api_value)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PackAPI::Mapping
|
|
4
|
+
###
|
|
5
|
+
# Specialized attribute transformer converting attribute Hashes
|
|
6
|
+
#
|
|
7
|
+
# Does not work with models (only Hashes)
|
|
8
|
+
# Does not convert values
|
|
9
|
+
# Converts model attributes to API attributes
|
|
10
|
+
# Converts API attributes to model attributes
|
|
11
|
+
class AttributeHashTransformer < AbstractTransformer
|
|
12
|
+
|
|
13
|
+
def execute
|
|
14
|
+
options.fetch(:contains_model_attributes, true) ?
|
|
15
|
+
model_attributes_to_api_attributes :
|
|
16
|
+
api_attributes_to_model_attributes
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
protected
|
|
20
|
+
|
|
21
|
+
def api_attributes_to_model_attributes
|
|
22
|
+
attribute_names = NormalizedAPIAttribute.new(api_attribute_names)
|
|
23
|
+
result = {}
|
|
24
|
+
data_source.each_key do |api_attribute|
|
|
25
|
+
normalized_api_attribute = attribute_names.normalize(api_attribute)
|
|
26
|
+
next unless mappings.key?(normalized_api_attribute)
|
|
27
|
+
|
|
28
|
+
model_attribute = model_attribute(normalized_api_attribute)
|
|
29
|
+
result[model_attribute] = data_source[api_attribute]
|
|
30
|
+
end
|
|
31
|
+
result
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def model_attributes_to_api_attributes
|
|
35
|
+
reversed_mappings = mappings.invert
|
|
36
|
+
result = {}
|
|
37
|
+
data_source.each_key do |model_attribute|
|
|
38
|
+
next unless reversed_mappings.key?(model_attribute)
|
|
39
|
+
|
|
40
|
+
api_attribute = reversed_mappings[model_attribute]
|
|
41
|
+
result[api_attribute] = data_source[model_attribute]
|
|
42
|
+
end
|
|
43
|
+
result
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|