Class: RestEasy::Resource
- Inherits:
-
Object
- Object
- RestEasy::Resource
- Extended by:
- Dry::Configurable
- Includes:
- Types
- Defined in:
- lib/rest_easy/resource.rb
Defined Under Namespace
Classes: AttributeBlockDSL, ConfigureDSL, MetaCollector, ModelProxy, ShadowCopy
Constant Summary collapse
- String =
Shadow Ruby's built-in type names so that inside a regular class body (not Class.new blocks),
String,Integer, etc. resolve to Dry::Types equivalents with coercion and constraint support. Types::Coercible::String
- Integer =
Types::Coercible::Integer
- Float =
Types::Coercible::Float
- Boolean =
Types::Params::Bool
- Date =
Types::Params::Date
- TYPE_MAP =
Map Ruby's built-in classes to Dry::Types equivalents. Used by
attrto resolve types passed from Class.new blocks where constant lookup doesn't find our shadowed constants. { ::String => Types::Coercible::String, ::Integer => Types::Coercible::Integer, ::Float => Types::Coercible::Float }.freeze
Instance Attribute Summary collapse
-
#meta ⇒ Object
readonly
Returns the value of attribute meta.
Class Method Summary collapse
- .after_parse(&block) ⇒ Object
- .after_serialise(&block) ⇒ Object
- .all ⇒ Object
- .all_attribute_definitions ⇒ Object
- .all_ignored_fields ⇒ Object
-
.attr(name_or_mapping, *args, &block) ⇒ Object
-- attr ----------------------------------------------------------.
-
.attribute_convention(value = nil) ⇒ Object
-- attribute_convention (deprecated) -------------------------------.
-
.attributes ⇒ Object
── Attribute introspection ────────────────────────────────────────.
- .attributes_with_flag(flag) ⇒ Object
-
.before_parse(&block) ⇒ Object
-- hooks ---------------------------------------------------------.
- .before_serialise(&block) ⇒ Object
- .configure(&block) ⇒ Object
- .create(instance) ⇒ Object
- .delete(id) ⇒ Object
-
.find(id) ⇒ Object
CRUD operations.
-
.get(path:, params: {}, headers: {}) ⇒ Object
HTTP primitives — delegate to the parent API module's connection.
-
.ignore(*api_field_names) ⇒ Object
-- ignore --------------------------------------------------------.
-
.json_attribute_converter ⇒ Object
-- conversions ---------------------------------------------------.
-
.key(attr_name, type = nil, *flags) ⇒ Object
-- key -----------------------------------------------------------.
- .key_attribute_name ⇒ Object
-
.metadata(**kwargs) ⇒ Object
-- metadata ------------------------------------------------------.
-
.parse(api_data) ⇒ Object
── Class-level operations ─────────────────────────────────────────.
- .post(path:, body: nil, headers: {}) ⇒ Object
- .put(path:, body: nil, headers: {}) ⇒ Object
- .query_parameter_converter ⇒ Object
- .resolve_after_parse_hook ⇒ Object
- .resolve_after_serialise_hook ⇒ Object
-
.resolve_before_parse_hook ⇒ Object
── Hook lookup (walks ancestor chain) ─────────────────────────────.
- .resolve_before_serialise_hook ⇒ Object
- .save(instance) ⇒ Object
-
.settings(&block) ⇒ Object
-- settings -------------------------------------------------------.
- .stub(**model_data) ⇒ Object
- .stub_defaults ⇒ Object
- .update(instance) ⇒ Object
-
.with_stub(**defaults) ⇒ Object
-- with_stub -----------------------------------------------------.
Instance Method Summary collapse
- #==(other) ⇒ Object (also: #eql?)
- #__changes__ ⇒ Object
- #api ⇒ Object
-
#config ⇒ Object
Delegate class-level config so hooks can call it via instance_exec.
- #hash ⇒ Object
-
#initialize(model_data = {}) ⇒ Resource
constructor
A new instance of Resource.
- #model ⇒ Object
- #serialise ⇒ Object
- #to_api ⇒ Object
- #to_json(*_args) ⇒ Object
- #unique_id ⇒ Object
- #update(changes = {}, **kwargs) ⇒ Object
Constructor Details
#initialize(model_data = {}) ⇒ Resource
Returns a new instance of Resource.
567 568 569 |
# File 'lib/rest_easy/resource.rb', line 567 def initialize(model_data = {}) init_from_model(model_data) end |
Instance Attribute Details
#meta ⇒ Object (readonly)
Returns the value of attribute meta.
565 566 567 |
# File 'lib/rest_easy/resource.rb', line 565 def @meta end |
Class Method Details
.after_parse(&block) ⇒ Object
373 374 375 |
# File 'lib/rest_easy/resource.rb', line 373 def after_parse(&block) @after_parse_hook = block end |
.after_serialise(&block) ⇒ Object
381 382 383 |
# File 'lib/rest_easy/resource.rb', line 381 def after_serialise(&block) @after_serialise_hook = block end |
.all ⇒ Object
475 476 477 478 |
# File 'lib/rest_easy/resource.rb', line 475 def all response = get(path: config.path.to_s) parse(response) end |
.all_attribute_definitions ⇒ Object
397 398 399 400 |
# File 'lib/rest_easy/resource.rb', line 397 def all_attribute_definitions parent = superclass.respond_to?(:all_attribute_definitions) ? superclass.all_attribute_definitions : {} parent.merge(own_attribute_definitions) end |
.all_ignored_fields ⇒ Object
406 407 408 409 |
# File 'lib/rest_easy/resource.rb', line 406 def all_ignored_fields parent = superclass.respond_to?(:all_ignored_fields) ? superclass.all_ignored_fields : [] parent + own_ignored_fields end |
.attr(name_or_mapping, *args, &block) ⇒ Object
-- attr ----------------------------------------------------------
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 |
# File 'lib/rest_easy/resource.rb', line 222 def attr(name_or_mapping, *args, &block) # Determine attribute_api_name and attribute_model_name if name_or_mapping.is_a?(::Array) attribute_model_name = name_or_mapping[0].to_sym attribute_api_name = name_or_mapping[1].to_s else attribute_model_name = name_or_mapping.to_sym attribute_api_name = json_attribute_converter.serialise(attribute_model_name) end # Extract type (non-Symbol), flags (Symbols), and optional mapper object type = nil flags = [] mapper = nil args.each do |arg| if arg.is_a?(::Symbol) flags << arg elsif arg.respond_to?(:parse) && arg.respond_to?(:serialise) mapper = arg else type = resolve_type(arg) end end raise AttributeError, "Attribute :#{attribute_model_name} must have a type" if type.nil? # Handle mapper object or block DSL for custom parse/serialise parse_block = nil serialise_block = nil source_fields = [] target_fields = [] if mapper parse_block = mapper.method(:parse) serialise_block = mapper.method(:serialise) # Introspect mapper method parameters the same way we do blocks. # This enables merge/split patterns with mapper objects. parse_params = parse_block.parameters.select { |ptype, _| ptype == :opt || ptype == :req } if parse_params.length > 1 flags << :synthetic unless flags.include?(:synthetic) source_fields = parse_params.map { |_, pname| pname } end serialise_params = serialise_block.parameters.select { |ptype, _| ptype == :opt || ptype == :req } if serialise_params.length > 1 flags << :synthetic unless flags.include?(:synthetic) target_fields = serialise_params.map { |_, pname| pname } end elsif block block_params = block.parameters.select { |ptype, _| ptype == :opt || ptype == :req } if block_params.any? # Bare block with params = implicit parse block. # The parameter names are API field references (resolved via convention). parse_block = block source_fields = block_params.map { |_, pname| pname } flags << :synthetic unless flags.include?(:synthetic) else # DSL block — evaluate to extract parse/serialise sub-blocks dsl = AttributeBlockDSL.new dsl.instance_eval(&block) parse_block = dsl.parse_block serialise_block = dsl.serialise_block # Introspect parse block parameters: if 2+ params, this is a # synthetic attribute. The parameter names are the source API fields # (e.g. |first_name, last_name| → source_fields [:first_name, :last_name]). if parse_block params = parse_block.parameters.select { |ptype, _| ptype == :opt || ptype == :req } if params.length > 1 flags << :synthetic unless flags.include?(:synthetic) source_fields = params.map { |_, pname| pname } end end # Introspect serialise block parameters: if 2+ params, the parameter # names are model field references to gather during serialisation. if serialise_block params = serialise_block.parameters.select { |ptype, _| ptype == :opt || ptype == :req } if params.length > 1 flags << :synthetic unless flags.include?(:synthetic) target_fields = params.map { |_, pname| pname } end end # Combine pattern (multi-param serialise, no multi-param parse) # has no inbound api_name on parse. If the user also wrote an # explicit `parse` block, it will be silently ignored — warn so # the inconsistency is visible at load time. if target_fields.any? && source_fields.empty? && parse_block warn "RestEasy: :#{attribute_model_name} declares a combine pattern " \ "(serialise from #{target_fields.inspect}) and also defines a parse block. " \ "Combine attributes have no inbound API field to read, so the parse block " \ "will not run. Remove the parse block, or restructure the declaration if you " \ "intended to read from the API." end end end # Handle :key flag if flags.include?(:key) if @key_attribute_name && @key_attribute_name != attribute_model_name warn "Warning: :#{@key_attribute_name} already defined as :key, ignoring :#{attribute_model_name} as :key" else @key_attribute_name = attribute_model_name end end # Register attribute definition own_attribute_definitions[attribute_model_name] = Attribute.new( model_name: attribute_model_name, api_name: attribute_api_name, type:, flags:, parse_block:, serialise_block:, source_fields:, target_fields: ) # Define accessor method on the class define_method(attribute_model_name) { @model_attributes[attribute_model_name] } end |
.attribute_convention(value = nil) ⇒ Object
-- attribute_convention (deprecated) -------------------------------
185 186 187 188 189 190 191 |
# File 'lib/rest_easy/resource.rb', line 185 def attribute_convention(value = nil) if value warn "RestEasy: attribute_convention is deprecated, use `configure { conversions.json_attributes = #{value.inspect} }` instead" config.conversions.json_attributes = value end json_attribute_converter end |
.attributes ⇒ Object
── Attribute introspection ────────────────────────────────────────
393 394 395 |
# File 'lib/rest_easy/resource.rb', line 393 def attributes all_attribute_definitions.keys end |
.attributes_with_flag(flag) ⇒ Object
402 403 404 |
# File 'lib/rest_easy/resource.rb', line 402 def attributes_with_flag(flag) all_attribute_definitions.select { |_, attr_def| attr_def.flags.include?(flag) } end |
.before_parse(&block) ⇒ Object
-- hooks ---------------------------------------------------------
369 370 371 |
# File 'lib/rest_easy/resource.rb', line 369 def before_parse(&block) @before_parse_hook = block end |
.before_serialise(&block) ⇒ Object
377 378 379 |
# File 'lib/rest_easy/resource.rb', line 377 def before_serialise(&block) @before_serialise_hook = block end |
.configure(&block) ⇒ Object
152 153 154 155 |
# File 'lib/rest_easy/resource.rb', line 152 def configure(&block) dsl = ConfigureDSL.new(config) dsl.instance_eval(&block) end |
.create(instance) ⇒ Object
488 489 490 491 492 493 494 |
# File 'lib/rest_easy/resource.rb', line 488 def create(instance) response = post( path: "#{config.path}", body: instance.serialise ) parse(response) end |
.delete(id) ⇒ Object
504 505 506 |
# File 'lib/rest_easy/resource.rb', line 504 def delete(id) parent.delete(path: "#{config.path}/#{id}") end |
.find(id) ⇒ Object
CRUD operations
470 471 472 473 |
# File 'lib/rest_easy/resource.rb', line 470 def find(id) response = get(path: "#{config.path}/#{id}") parse(response) end |
.get(path:, params: {}, headers: {}) ⇒ Object
HTTP primitives — delegate to the parent API module's connection
510 511 512 513 514 |
# File 'lib/rest_easy/resource.rb', line 510 def get(path:, params: {}, headers: {}) converter = query_parameter_converter converted_params = converter ? params.transform_keys { |k| converter.serialise(k) } : params parent.get(path:, params: converted_params, headers:) end |
.ignore(*api_field_names) ⇒ Object
-- ignore --------------------------------------------------------
361 362 363 364 365 |
# File 'lib/rest_easy/resource.rb', line 361 def ignore(*api_field_names) api_field_names.each do |field_name| own_ignored_fields << field_name.to_sym end end |
.json_attribute_converter ⇒ Object
-- conversions ---------------------------------------------------
169 170 171 172 173 174 175 |
# File 'lib/rest_easy/resource.rb', line 169 def json_attribute_converter Conventions.resolve( config.conversions.json_attributes || parent&.config&.conversions&.json_attributes || Conventions::DEFAULT ) end |
.key(attr_name, type = nil, *flags) ⇒ Object
-- key -----------------------------------------------------------
348 349 350 351 352 353 354 355 356 357 |
# File 'lib/rest_easy/resource.rb', line 348 def key(attr_name, type = nil, *flags) if @key_attribute_name warn "Warning: key already defined as :#{@key_attribute_name}, overriding with :#{attr_name}" end if type self.attr(attr_name, type, *flags, :key) else self.attr(attr_name, *flags, :key) end end |
.key_attribute_name ⇒ Object
411 412 413 414 |
# File 'lib/rest_easy/resource.rb', line 411 def key_attribute_name @key_attribute_name || (superclass.respond_to?(:key_attribute_name) ? superclass.key_attribute_name : nil) end |
.metadata(**kwargs) ⇒ Object
-- metadata ------------------------------------------------------
159 160 161 162 163 164 165 |
# File 'lib/rest_easy/resource.rb', line 159 def (**kwargs) if kwargs.any? .merge!(kwargs) else end end |
.parse(api_data) ⇒ Object
── Class-level operations ─────────────────────────────────────────
445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 |
# File 'lib/rest_easy/resource.rb', line 445 def parse(api_data) = MetaCollector.new hook = resolve_before_parse_hook if hook api_data = instance_exec(api_data, , &hook) end = .to_h if api_data.is_a?(::Array) api_data.map { |item| allocate.tap { |instance| instance.send(:init_from_api, item, ) } } else allocate.tap { |instance| instance.send(:init_from_api, api_data, ) } end end |
.post(path:, body: nil, headers: {}) ⇒ Object
516 517 518 |
# File 'lib/rest_easy/resource.rb', line 516 def post(path:, body: nil, headers: {}) parent.post(path:, body:, headers:) end |
.put(path:, body: nil, headers: {}) ⇒ Object
520 521 522 |
# File 'lib/rest_easy/resource.rb', line 520 def put(path:, body: nil, headers: {}) parent.put(path:, body:, headers:) end |
.query_parameter_converter ⇒ Object
177 178 179 180 181 |
# File 'lib/rest_easy/resource.rb', line 177 def query_parameter_converter convention = config.conversions.query_parameters || parent&.config&.conversions&.query_parameters convention && Conventions.resolve(convention) end |
.resolve_after_parse_hook ⇒ Object
428 429 430 431 |
# File 'lib/rest_easy/resource.rb', line 428 def resolve_after_parse_hook @after_parse_hook || (superclass.respond_to?(:resolve_after_parse_hook) ? superclass.resolve_after_parse_hook : nil) end |
.resolve_after_serialise_hook ⇒ Object
438 439 440 441 |
# File 'lib/rest_easy/resource.rb', line 438 def resolve_after_serialise_hook @after_serialise_hook || (superclass.respond_to?(:resolve_after_serialise_hook) ? superclass.resolve_after_serialise_hook : nil) end |
.resolve_before_parse_hook ⇒ Object
── Hook lookup (walks ancestor chain) ─────────────────────────────
423 424 425 426 |
# File 'lib/rest_easy/resource.rb', line 423 def resolve_before_parse_hook @before_parse_hook || (superclass.respond_to?(:resolve_before_parse_hook) ? superclass.resolve_before_parse_hook : nil) end |
.resolve_before_serialise_hook ⇒ Object
433 434 435 436 |
# File 'lib/rest_easy/resource.rb', line 433 def resolve_before_serialise_hook @before_serialise_hook || (superclass.respond_to?(:resolve_before_serialise_hook) ? superclass.resolve_before_serialise_hook : nil) end |
.save(instance) ⇒ Object
480 481 482 483 484 485 486 |
# File 'lib/rest_easy/resource.rb', line 480 def save(instance) if instance..new? create(instance) else update(instance) end end |
.settings(&block) ⇒ Object
-- settings -------------------------------------------------------
146 147 148 149 150 |
# File 'lib/rest_easy/resource.rb', line 146 def settings(&block) return super() unless block class_eval(&block) end |
.stub(**model_data) ⇒ Object
462 463 464 465 466 |
# File 'lib/rest_easy/resource.rb', line 462 def stub(**model_data) defaults = stub_defaults || {} data = defaults.merge(model_data) allocate.tap { |instance| instance.send(:init_from_model, data) } end |
.stub_defaults ⇒ Object
416 417 418 419 |
# File 'lib/rest_easy/resource.rb', line 416 def stub_defaults parent = superclass.respond_to?(:stub_defaults) ? superclass.stub_defaults : {} (parent || {}).merge(@stub_defaults || {}) end |
.update(instance) ⇒ Object
496 497 498 499 500 501 502 |
# File 'lib/rest_easy/resource.rb', line 496 def update(instance) response = put( path: "#{config.path}/#{instance.unique_id}", body: instance.serialise ) parse(response) end |
.with_stub(**defaults) ⇒ Object
-- with_stub -----------------------------------------------------
387 388 389 |
# File 'lib/rest_easy/resource.rb', line 387 def with_stub(**defaults) @stub_defaults = defaults end |
Instance Method Details
#==(other) ⇒ Object Also known as: eql?
681 682 683 684 |
# File 'lib/rest_easy/resource.rb', line 681 def ==(other) other.is_a?(self.class) && self.class == other.class && @model_attributes == other.send(:model_attributes_hash) end |
#__changes__ ⇒ Object
605 606 607 |
# File 'lib/rest_easy/resource.rb', line 605 def __changes__ @changes || {} end |
#api ⇒ Object
575 576 577 |
# File 'lib/rest_easy/resource.rb', line 575 def api ShadowCopy.new(@api_data) end |
#config ⇒ Object
Delegate class-level config so hooks can call it via instance_exec
561 562 563 |
# File 'lib/rest_easy/resource.rb', line 561 def config self.class.config end |
#hash ⇒ Object
688 689 690 |
# File 'lib/rest_easy/resource.rb', line 688 def hash [self.class, @model_attributes].hash end |
#model ⇒ Object
571 572 573 |
# File 'lib/rest_easy/resource.rb', line 571 def model ModelProxy.new(@model_attributes) end |
#serialise ⇒ Object
609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 |
# File 'lib/rest_easy/resource.rb', line 609 def serialise klass = self.class # Run before_serialise hook on the instance # Input: model_attributes. Side-effect only; return value ignored. hook = klass.resolve_before_serialise_hook instance_exec(@model_attributes, &hook) if hook result = {} # Serialise all attributes klass.all_attribute_definitions.each do |_model_name, attr_def| next if attr_def.read_only? if attr_def.target_fields.any? # Multi-param serialise: gather model values by param names, splat into block model_values = attr_def.target_fields.map { |fn| @model_attributes[fn] } attr_def.validate_required!(*model_values) result[attr_def.api_name] = attr_def.serialise_value(*model_values) elsif attr_def.source_fields.any? value = @model_attributes[attr_def.model_name] attr_def.validate_required!(value) serialised = attr_def.serialise_value(value) if serialised.is_a?(::Array) # Array return: zip with source field API names convention = klass.json_attribute_converter attr_def.source_fields.zip(serialised).each do |field_name, field_value| api_key = convention.serialise(field_name) result[api_key] = field_value end elsif serialised.is_a?(::Hash) # Hash return: merge into result result.merge!(serialised) else result[attr_def.api_name] = serialised end else value = @model_attributes[attr_def.model_name] attr_def.validate_required!(value) result[attr_def.api_name] = attr_def.serialise_value(value) end end # Merge ignored fields from shadow copy if @api_data && !@api_data.empty? known_api_names = klass.all_attribute_definitions.values.map(&:api_name) @api_data.each do |api_key, value| unless known_api_names.include?(api_key) || result.key?(api_key) result[api_key] = value end end end # Run after_serialise hook on the instance # Input: serialised_data, model. Output: final serialised_data. hook = klass.resolve_after_serialise_hook if hook result = instance_exec(result, model, &hook) end result end |
#to_api ⇒ Object
677 678 679 |
# File 'lib/rest_easy/resource.rb', line 677 def to_api ::JSON.generate(serialise) end |
#to_json(*_args) ⇒ Object
672 673 674 675 |
# File 'lib/rest_easy/resource.rb', line 672 def to_json(*_args) model_hash = @model_attributes.transform_keys(&:to_s) ::JSON.generate(model_hash) end |
#unique_id ⇒ Object
579 580 581 582 |
# File 'lib/rest_easy/resource.rb', line 579 def unique_id key_name = self.class.key_attribute_name key_name ? @model_attributes[key_name] : nil end |
#update(changes = {}, **kwargs) ⇒ Object
584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 |
# File 'lib/rest_easy/resource.rb', line 584 def update(changes = {}, **kwargs) changes = changes.merge(kwargs) unless kwargs.empty? return self if changes.empty? klass = self.class coerced = {} changes.each do |attr_name, value| attr_def = klass.all_attribute_definitions[attr_name] coerced[attr_name] = if attr_def && !value.nil? attr_def.coerce(value) else value end end new_model = @model_attributes.merge(coerced) new_instance = self.class.allocate new_instance.send(:init_from_update, new_model, @api_data, coerced) new_instance end |