11import Foundation
2- import WordPressKit
32import WordPressShared
43
5- /// A convenience struct that provides CRUD operations on post metadata.
6- ///
7- /// ## WordPress Metadata Overview
8- ///
9- /// WordPress stores custom metadata as key-value pairs associated with posts.
10- /// Each metadata item contains a string key, a value (which can be any
11- /// JSON-serializable type), and an optional ID for database tracking.
12- ///
13- /// ## Expected Format
14- ///
15- /// Metadata is stored as a JSON array of dictionaries, where each dictionary represents one
16- /// metadata item:
17- ///
18- /// ```json
19- /// [
20- /// {
21- /// "key": "_jetpack_newsletter_access",
22- /// "value": "subscribers",
23- /// "id": "123"
24- /// },
25- /// {
26- /// "key": "custom_field",
27- /// "value": "some value"
28- /// }
29- /// ]
30- /// ```
31- public struct PostMetadata {
32- public struct Key : ExpressibleByStringLiteral , Hashable {
33- public let rawValue : String
34-
35- public init ( rawValue: String ) {
36- self . rawValue = rawValue
37- }
38-
39- public init ( stringLiteral value: String ) {
40- self . rawValue = value
41- }
42- }
43-
44- enum Error : Swift . Error {
45- case invalidData
46- }
47-
48- // Raw JSON dictionaries, keyed by metadata key
49- private var items : [ Key : [ String : Any ] ] = [ : ]
4+ /// Metadata used by the app.
5+ public struct PostMetadata : Hashable {
6+ /// Gets or sets the Jetpack Newsletter access level as a PostAccessLevel enum
7+ public var accessLevel : JetpackPostAccessLevel ?
508
51- /// Returns all metadata as a dictionary (alias for allItems)
52- public var values : [ [ String : Any ] ] {
53- Array ( items. values)
54- }
9+ /// Returns `true` if the post is configured to _not_ be sent in an email
10+ /// to subscribers.
11+ public var isJetpackNewsletterEmailDisabled : Bool
5512
5613 /// Initialized metadata with the given post.
5714 public init ( _ post: AbstractPost ) {
58- if let data = post. rawMetadata {
59- do {
60- let metadata = try JSONSerialization . jsonObject ( with: data) as? [ [ String : Any ] ] ?? [ ]
61- self = PostMetadata ( metadata: metadata)
62- } catch {
63- wpAssertionFailure ( " Failed to decode metadata JSON " , userInfo: [ " error " : error. localizedDescription] )
64- self = PostMetadata ( )
65- }
66- } else {
67- self = PostMetadata ( )
68- }
15+ self = PostMetadata ( from: PostMetadataContainer ( post) )
6916 }
7017
71- /// Initialize with raw metadata Data (non-throwing version for backward compatibility)
72- /// If the data is invalid, creates an empty PostMetadata
73- ///
74- /// - Parameter data: The JSON data containing metadata array
75- public init ( data: Data ) throws {
76- let metadata = try JSONSerialization . jsonObject ( with: data)
77- guard let dictionary = metadata as? [ [ String : Any ] ] else {
78- throw Error . invalidData
79- }
80- self = PostMetadata ( metadata: dictionary)
18+ public init ( from container: PostMetadataContainer ) {
19+ self . accessLevel = container. accessLevel
20+ self . isJetpackNewsletterEmailDisabled = container. getAdaptiveBool ( for: . jetpackNewsletterEmailDisabled)
8121 }
8222
83- /// Initialize with raw metadata array (same format as JSON data)
84- /// - Parameter metadata: Array of metadata dictionaries with "key", "value", and optional "id"
85- public init ( metadata: [ [ String : Any ] ] = [ ] ) {
86- for item in metadata {
87- if let key = item [ " key " ] as? String {
88- self . items [ Key ( rawValue: key) ] = item
89- }
23+ /// Applies the metadata values to the container and returns them
24+ /// as metadata values.
25+ public func encode( in container: inout PostMetadataContainer ) {
26+ let previous = PostMetadata ( from: container)
27+ if previous. accessLevel != accessLevel {
28+ container. accessLevel = accessLevel
9029 }
91- }
92-
93- // MARK: - Encoding
94-
95- /// Encodes the metadata back to Data for storage in rawMetadata
96- /// - Returns: JSON Data representation of the metadata, or nil if empty
97- public func encode( ) throws -> Data {
98- do {
99- return try JSONSerialization . data ( withJSONObject: Array ( items. values) , options: [ ] )
100- } catch {
101- wpAssertionFailure ( " Failed to encode metadata to JSON " , userInfo: [ " error " : error. localizedDescription] )
102- throw error
30+ if previous. isJetpackNewsletterEmailDisabled != isJetpackNewsletterEmailDisabled {
31+ container. setValue ( String ( describing: isJetpackNewsletterEmailDisabled) , for: . jetpackNewsletterEmailDisabled)
10332 }
10433 }
10534
106- // MARK: - CRUD
107-
108- /// Retrieves a metadata value by key with generic type casting
109- /// - Parameters:
110- /// - expectedType: The expected type of the value
111- /// - key: The metadata key to search for
112- /// - Returns: The value cast to the specified type if found and compatible, nil otherwise
113- public func getValue< T> ( _ expectedType: T . Type , forKey key: Key ) -> T ? {
114- guard let dict = items [ key] , let value = dict [ " value " ] else { return nil }
115- guard let value = value as? T else {
116- wpAssertionFailure ( " unexpected value " , userInfo: [
117- " key " : key. rawValue,
118- " actual_type " : String ( describing: expectedType) ,
119- " expected_type " : String ( describing: type ( of: value) )
120- ] )
121- return nil
122- }
123- return value
124- }
125-
126- /// Retrieves a metadata value by key as String (convenience method)
127- /// - Parameter key: The metadata key to search for
128- /// - Returns: The value as String if found and convertible, nil otherwise
129- public func getString( for key: Key ) -> String ? {
130- getValue ( String . self, forKey: key)
131- }
132-
133- /// Sets or updates a metadata item with any JSON-compatible value
134- /// - Parameters:
135- /// - value: The metadata value (must be JSON-compatible)
136- /// - key: The metadata key
137- /// - id: Optional metadata ID
138- public mutating func setValue( _ value: Any , for key: Key , id: String ? = nil ) {
139- var dict : [ String : Any ] = [
140- " key " : key. rawValue,
141- " value " : value
142- ]
143- // Preserve existing ID if not provided
144- if let id {
145- dict [ " id " ] = id
146- } else if let existingDict = items [ key] , let existingID = existingDict [ " id " ] {
147- dict [ " id " ] = existingID
148- }
149- guard JSONSerialization . isValidJSONObject ( dict) else {
150- return wpAssertionFailure ( " invalid value " , userInfo: [ " type " : String ( describing: type ( of: value) ) ] )
151- }
152- items [ key] = dict
153- }
154-
155- /// Removes a metadata item by key
156- /// - Parameter key: The metadata key to remove
157- /// - Returns: True if the item was found and removed, false otherwise
158- @discardableResult
159- public mutating func removeValue( for key: Key ) -> Bool {
160- items. removeValue ( forKey: key) != nil
161- }
162-
163- /// Clears all metadata
164- public mutating func clear( ) {
165- items. removeAll ( )
166- }
167-
168- /// Returns the complete dictionary entry for the given key.
35+ /// Returns all metadata values encoded in `PostMetadataContainer` as
36+ /// WordPress metadata fields.
16937 ///
170- /// - Parameter key: The metadata key to retrieve
171- /// - Returns: The complete metadata dictionary containing "key", "value", and optional "id", or nil if not found
172- public func entry ( forKey key : Key ) -> [ String : Any ] ? {
173- return items [ key ]
38+ /// - note: It returns _only_ the fields managed by the app so that we
39+ /// don't send more than needed to the server when updating it.
40+ public static func entries ( in container : PostMetadataContainer ) -> [ [ String : Any ] ] {
41+ PostMetadata . allKeys . compactMap ( container . entry )
17442 }
175- }
17643
177- // MARK: - PostMetadata (Jetpack)
44+ /// Returns all keys managed by the app.
45+ public static let allKeys : [ PostMetadataContainer . Key ] = [
46+ . jetpackNewsletterAccess,
47+ . jetpackNewsletterEmailDisabled
48+ ]
49+ }
17850
179- extension PostMetadata . Key {
180- /// Jetpack Newsletter access level metadata key
181- public static let jetpackNewsletterAccess : PostMetadata . Key = " _jetpack_newsletter_access "
51+ /// Valid access levels for Jetpack Newsletter
52+ public enum JetpackPostAccessLevel : String , CaseIterable , Hashable , Codable {
53+ case everybody = " everybody "
54+ case subscribers = " subscribers "
55+ case paidSubscribers = " paid_subscribers "
18256}
18357
184- extension PostMetadata {
185- /// Gets or sets the Jetpack Newsletter access level as a PostAccessLevel enum
186- public var accessLevel : JetpackPostAccessLevel ? {
58+ private extension PostMetadataContainer {
59+ var accessLevel : JetpackPostAccessLevel ? {
18760 get {
18861 guard let value = getString ( for: . jetpackNewsletterAccess) else { return nil }
18962 return JetpackPostAccessLevel ( rawValue: value)
@@ -198,9 +71,7 @@ extension PostMetadata {
19871 }
19972}
20073
201- /// Valid access levels for Jetpack Newsletter
202- public enum JetpackPostAccessLevel : String , CaseIterable , Hashable , Codable {
203- case everybody = " everybody "
204- case subscribers = " subscribers "
205- case paidSubscribers = " paid_subscribers "
74+ extension PostMetadataContainer . Key {
75+ static let jetpackNewsletterAccess : PostMetadataContainer . Key = " _jetpack_newsletter_access "
76+ static let jetpackNewsletterEmailDisabled : PostMetadataContainer . Key = " _jetpack_dont_email_post_to_subs "
20677}
0 commit comments